]>
Commit | Line | Data |
---|---|---|
866f8763 A |
1 | /* |
2 | * Copyright (c) 2017 Apple Inc. All Rights Reserved. | |
3 | * | |
4 | * @APPLE_LICENSE_HEADER_START@ | |
5 | * | |
6 | * This file contains Original Code and/or Modifications of Original Code | |
7 | * as defined in and that are subject to the Apple Public Source License | |
8 | * Version 2.0 (the 'License'). You may not use this file except in | |
9 | * compliance with the License. Please obtain a copy of the License at | |
10 | * http://www.opensource.apple.com/apsl/ and read it before using this | |
11 | * file. | |
12 | * | |
13 | * The Original Code and all software distributed under the License are | |
14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER | |
15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, | |
16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. | |
18 | * Please see the License for the specific language governing rights and | |
19 | * limitations under the License. | |
20 | * | |
21 | * @APPLE_LICENSE_HEADER_END@ | |
22 | */ | |
23 | ||
24 | #import <Foundation/Foundation.h> | |
25 | ||
26 | #if OCTAGON | |
27 | ||
28 | #import <dispatch/dispatch.h> | |
29 | ||
30 | #import "keychain/ckks/CKKSZoneChangeFetcher.h" | |
31 | #import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h" | |
32 | #import "keychain/ckks/CKKSKeychainView.h" | |
33 | #import "keychain/ckks/CKKSNearFutureScheduler.h" | |
34 | #import "keychain/ckks/CloudKitCategories.h" | |
35 | ||
36 | CKKSFetchBecause* const CKKSFetchBecauseAPNS = (CKKSFetchBecause*) @"apns"; | |
37 | CKKSFetchBecause* const CKKSFetchBecauseAPIFetchRequest = (CKKSFetchBecause*) @"api"; | |
38 | CKKSFetchBecause* const CKKSFetchBecauseCurrentItemFetchRequest = (CKKSFetchBecause*) @"currentitemcheck"; | |
39 | CKKSFetchBecause* const CKKSFetchBecauseInitialStart = (CKKSFetchBecause*) @"initialfetch"; | |
40 | CKKSFetchBecause* const CKKSFetchBecauseSecuritydRestart = (CKKSFetchBecause*) @"restart"; | |
41 | CKKSFetchBecause* const CKKSFetchBecausePreviousFetchFailed = (CKKSFetchBecause*) @"fetchfailed"; | |
42 | CKKSFetchBecause* const CKKSFetchBecauseKeyHierarchy = (CKKSFetchBecause*) @"keyhierarchy"; | |
43 | CKKSFetchBecause* const CKKSFetchBecauseTesting = (CKKSFetchBecause*) @"testing"; | |
44 | ||
45 | @interface CKKSZoneChangeFetcher () | |
46 | @property NSString* name; | |
47 | @property dispatch_queue_t queue; | |
48 | ||
49 | @property CKKSFetchAllRecordZoneChangesOperation* currentFetch; | |
50 | @property CKKSResultOperation* currentProcessResult; | |
51 | ||
52 | @property NSMutableSet<CKKSFetchBecause*>* currentFetchReasons; | |
53 | @property bool newRequests; // true if there's someone pending on successfulFetchDependency | |
54 | @property bool newResyncRequests; // true if someone asked for a refetch operation | |
55 | @property CKKSResultOperation* successfulFetchDependency; | |
56 | ||
57 | @property CKKSNearFutureScheduler* fetchScheduler; | |
58 | @end | |
59 | ||
60 | @implementation CKKSZoneChangeFetcher | |
61 | ||
62 | - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks { | |
63 | if((self = [super init])) { | |
64 | _ckks = ckks; | |
65 | _zoneID = ckks.zoneID; | |
66 | ||
67 | _currentFetchReasons = [[NSMutableSet alloc] init]; | |
68 | ||
69 | _name = [NSString stringWithFormat:@"zone-change-fetcher-%@", _zoneID.zoneName]; | |
70 | _queue = dispatch_queue_create([_name UTF8String], DISPATCH_QUEUE_SERIAL); | |
71 | [self newSuccesfulFetchDependency]; | |
72 | ||
73 | _newRequests = false; | |
74 | ||
75 | // If we're testing, for the initial delay, use 0.2 second. Otherwise, 2s. | |
76 | dispatch_time_t initialDelay = (SecCKKSTestsEnabled() ? 200 * NSEC_PER_MSEC : 2 * NSEC_PER_SEC); | |
77 | ||
78 | // If we're testing, for the initial delay, use 2 second. Otherwise, 30s. | |
79 | dispatch_time_t continuingDelay = (SecCKKSTestsEnabled() ? 2 * NSEC_PER_SEC : 30 * NSEC_PER_SEC); | |
80 | ||
81 | __weak __typeof(self) weakSelf = self; | |
82 | _fetchScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat:@"zone-change-fetch-scheduler-%@", self.zoneID.zoneName] | |
83 | initialDelay:initialDelay | |
84 | continuingDelay:continuingDelay | |
85 | keepProcessAlive:false | |
86 | block:^{ | |
87 | [weakSelf maybeCreateNewFetch]; | |
88 | }]; | |
89 | } | |
90 | return self; | |
91 | } | |
92 | ||
93 | - (NSString*)description { | |
94 | NSDate* nextFetchAt = self.fetchScheduler.nextFireTime; | |
95 | if(nextFetchAt) { | |
96 | NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; | |
97 | [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; | |
98 | return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): next fetch at %@", self.name, [dateFormatter stringFromDate: nextFetchAt]]; | |
99 | } else { | |
100 | return [NSString stringWithFormat: @"<CKKSZoneChangeFetcher(%@): no pending fetches", self.name]; | |
101 | } | |
102 | } | |
103 | ||
104 | - (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why { | |
105 | return [self requestSuccessfulFetch:why resync:false]; | |
106 | } | |
107 | ||
108 | - (CKKSResultOperation*)requestSuccessfulResyncFetch:(CKKSFetchBecause*)why { | |
109 | return [self requestSuccessfulFetch:why resync:true]; | |
110 | } | |
111 | ||
112 | - (CKKSResultOperation*)requestSuccessfulFetch:(CKKSFetchBecause*)why resync:(bool)resync { | |
113 | __block CKKSResultOperation* dependency = nil; | |
114 | dispatch_sync(self.queue, ^{ | |
115 | dependency = self.successfulFetchDependency; | |
116 | self.newRequests = true; | |
117 | self.newResyncRequests |= resync; | |
118 | [self.currentFetchReasons addObject: why]; | |
119 | ||
120 | [self.fetchScheduler trigger]; | |
121 | }); | |
122 | ||
123 | return dependency; | |
124 | } | |
125 | ||
126 | -(void)maybeCreateNewFetch { | |
127 | dispatch_sync(self.queue, ^{ | |
128 | if(self.newRequests && | |
129 | (self.currentFetch == nil || [self.currentFetch isFinished]) && | |
130 | (self.currentProcessResult == nil || [self.currentProcessResult isFinished])) { | |
131 | [self _onqueueCreateNewFetch]; | |
132 | } | |
133 | }); | |
134 | } | |
135 | ||
136 | -(void)_onqueueCreateNewFetch { | |
137 | dispatch_assert_queue(self.queue); | |
138 | ||
139 | __weak __typeof(self) weakSelf = self; | |
140 | ||
141 | CKKSResultOperation* dependency = self.successfulFetchDependency; | |
142 | ||
143 | CKKSKeychainView* ckks = self.ckks; // take a strong reference | |
144 | if(!ckks) { | |
145 | secerror("ckksfetcher: received a null CKKSKeychainView pointer; strange."); | |
146 | return; | |
147 | } | |
148 | ||
149 | ckksnotice("ckksfetcher", self.zoneID, "Starting a new fetch for %@", self.zoneID.zoneName); | |
150 | ||
151 | NSSet* lastFetchReasons = self.currentFetchReasons; | |
152 | self.currentFetchReasons = [[NSMutableSet alloc] init]; | |
153 | if(self.newResyncRequests) { | |
154 | [lastFetchReasons setByAddingObject:@"resync"]; | |
155 | } | |
156 | ||
157 | CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName: [[lastFetchReasons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES]]] componentsJoinedByString:@","]]; | |
158 | ||
159 | CKKSFetchAllRecordZoneChangesOperation* fetchAllChanges = [[CKKSFetchAllRecordZoneChangesOperation alloc] initWithCKKSKeychainView: ckks ckoperationGroup:operationGroup]; | |
160 | fetchAllChanges.resync = self.newResyncRequests; | |
161 | self.newResyncRequests = false; | |
162 | ||
163 | // Can't fetch until the zone is setup. | |
164 | [fetchAllChanges addNullableDependency:ckks.zoneSetupOperation]; | |
165 | ||
166 | self.currentProcessResult = [CKKSResultOperation operationWithBlock: ^{ | |
167 | __strong __typeof(self) strongSelf = weakSelf; | |
168 | if(!strongSelf) { | |
169 | secerror("ckksfetcher: Received a null self pointer; strange."); | |
170 | return; | |
171 | } | |
172 | ||
173 | CKKSKeychainView* blockckks = strongSelf.ckks; // take a strong reference | |
174 | if(!blockckks) { | |
175 | secerror("ckksfetcher: Received a null CKKSKeychainView pointer; strange."); | |
176 | return; | |
177 | } | |
178 | ||
179 | dispatch_sync(strongSelf.queue, ^{ | |
180 | if(!fetchAllChanges.error) { | |
181 | // success! notify the listeners. | |
182 | [blockckks scheduleOperation: dependency]; | |
183 | ||
184 | // Did new people show up and want another fetch? | |
185 | if(strongSelf.newRequests) { | |
186 | [strongSelf.fetchScheduler trigger]; | |
187 | } | |
188 | } else { | |
189 | if([blockckks isFatalCKFetchError: fetchAllChanges.error]) { | |
190 | ckkserror("ckksfetcher", strongSelf.zoneID, "Notified that %@ is a fatal error. Not restarting fetch.", fetchAllChanges.error); | |
191 | return; | |
192 | } | |
193 | ||
194 | // The operation errored. Chain the dependency on the current one... | |
195 | [dependency addSuccessDependency: strongSelf.successfulFetchDependency]; | |
196 | [blockckks scheduleOperation: dependency]; | |
197 | ||
198 | // And in a bit, try the fetch again. | |
199 | NSNumber* delaySeconds = fetchAllChanges.error.userInfo[CKErrorRetryAfterKey]; | |
200 | if([fetchAllChanges.error.domain isEqual: CKErrorDomain] && delaySeconds) { | |
201 | ckksnotice("ckksfetcher", strongSelf.zoneID, "Fetch failed with rate-limiting error, restarting in %@ seconds: %@", delaySeconds, fetchAllChanges.error); | |
202 | [strongSelf.fetchScheduler waitUntil: NSEC_PER_SEC * [delaySeconds unsignedLongValue]]; | |
203 | } else { | |
204 | ckksnotice("ckksfetcher", strongSelf.zoneID, "Fetch failed with error, restarting soon: %@", fetchAllChanges.error); | |
205 | } | |
206 | ||
207 | // Add the failed fetch reasons to the new fetch reasons | |
208 | [strongSelf.currentFetchReasons unionSet:lastFetchReasons]; | |
209 | [strongSelf.currentFetchReasons addObject:CKKSFetchBecausePreviousFetchFailed]; | |
210 | strongSelf.newRequests = true; | |
211 | strongSelf.newResyncRequests |= fetchAllChanges.resync; | |
212 | [strongSelf.fetchScheduler trigger]; | |
213 | } | |
214 | }); | |
215 | }]; | |
216 | self.currentProcessResult.name = @"zone-change-fetcher-worker"; | |
217 | [self.currentProcessResult addDependency: fetchAllChanges]; | |
218 | ||
219 | [ckks scheduleOperation: self.currentProcessResult]; | |
220 | ||
221 | self.currentFetch = fetchAllChanges; | |
222 | [ckks scheduleOperation: self.currentFetch]; | |
223 | ||
224 | // creata a new fetch dependency, for all those who come in while this operation is executing | |
225 | self.newRequests = false; | |
226 | [self newSuccesfulFetchDependency]; | |
227 | } | |
228 | ||
229 | -(void)newSuccesfulFetchDependency { | |
230 | CKKSResultOperation* dep = [[CKKSResultOperation alloc] init]; | |
231 | __weak __typeof(dep) weakDep = dep; | |
232 | ||
233 | // Since these dependencies might chain, when one runs, break the chain. | |
234 | [dep addExecutionBlock:^{ | |
235 | __strong __typeof(dep) strongDep = weakDep; | |
236 | ||
237 | // Remove all dependencies | |
238 | NSArray* deps = [strongDep.dependencies copy]; | |
239 | for(NSOperation* op in deps) { | |
240 | [strongDep removeDependency: op]; | |
241 | } | |
242 | }]; | |
243 | dep.name = @"successful-fetch-dependency"; | |
244 | ||
245 | self.successfulFetchDependency = dep; | |
246 | } | |
247 | ||
248 | -(void)cancel { | |
249 | [self.fetchScheduler cancel]; | |
250 | } | |
251 | ||
252 | @end | |
253 | ||
254 | #endif | |
255 | ||
256 |