]>
Commit | Line | Data |
---|---|---|
b54c578e A |
1 | #if OCTAGON |
2 | ||
3 | #import <CloudKit/CloudKit_Private.h> | |
4 | ||
5 | #import "keychain/ckks/CloudKitCategories.h" | |
6 | #import "keychain/ckks/CKKS.h" | |
7 | #import "keychain/ckks/CKKSNearFutureScheduler.h" | |
8 | #import "keychain/ckks/CKKSReachabilityTracker.h" | |
9 | #import "keychain/ckks/CKKSZoneModifier.h" | |
10 | ||
11 | #import "utilities/debugging.h" | |
12 | #import "keychain/ot/ObjCImprovements.h" | |
13 | ||
14 | @implementation CKKSZoneModifyOperations | |
15 | ||
16 | - (instancetype)init | |
17 | { | |
18 | return nil; | |
19 | } | |
20 | ||
21 | - (instancetype)initWithZoneModificationOperation:(CKKSResultOperation*)zoneModificationOperationDependency | |
22 | zoneSubscriptionOperation:(CKKSResultOperation*)zoneSubscriptionOperationDependency | |
23 | { | |
24 | if((self = [super init])) { | |
25 | _zoneModificationOperation = zoneModificationOperationDependency; | |
26 | _zoneSubscriptionOperation = zoneSubscriptionOperationDependency; | |
27 | ||
28 | _zonesToCreate = [NSMutableArray array]; | |
29 | _subscriptionsToSubscribe = [NSMutableArray array]; | |
30 | _zoneIDsToDelete = [NSMutableArray array]; | |
31 | } | |
32 | return self; | |
33 | } | |
34 | @end | |
35 | ||
36 | // CKKSZoneModifier | |
37 | ||
38 | @interface CKKSZoneModifier () | |
39 | ||
40 | // Returned to clients that call createZone/deleteZone before it launches | |
41 | @property CKKSZoneModifyOperations* pendingOperations; | |
42 | ||
43 | @property bool halted; | |
44 | ||
45 | @property NSOperationQueue* operationQueue; | |
46 | @property dispatch_queue_t queue; | |
47 | ||
48 | // Used to linearize all CK operations to avoid tripping over our own feet | |
49 | @property NSHashTable<CKDatabaseOperation*>* ckOperations; | |
50 | @property CKKSZoneModifyOperations* inflightOperations; | |
51 | ||
52 | // Set to true if the last CK operation finished with a network error | |
53 | // Cleared if the last CK operation didn't | |
54 | // Used as an heuristic to wait on the reachability tracker | |
55 | @property bool networkFailure; | |
56 | @end | |
57 | ||
58 | @implementation CKKSZoneModifier | |
59 | - (instancetype)init | |
60 | { | |
61 | return nil; | |
62 | } | |
63 | ||
64 | - (instancetype)initWithContainer:(CKContainer*)container | |
65 | reachabilityTracker:(CKKSReachabilityTracker*)reachabilityTracker | |
66 | cloudkitDependencies:(CKKSCloudKitClassDependencies*)cloudKitClassDependencies | |
67 | { | |
68 | if((self = [super init])) { | |
69 | _container = container; | |
70 | _reachabilityTracker = reachabilityTracker; | |
71 | _cloudKitClassDependencies = cloudKitClassDependencies; | |
72 | ||
73 | _database = [_container privateCloudDatabase]; | |
74 | _operationQueue = [[NSOperationQueue alloc] init]; | |
75 | _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSZoneModifier.%@", container.containerIdentifier] UTF8String], | |
76 | DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); | |
77 | ||
78 | WEAKIFY(self); | |
79 | ||
80 | // This does double-duty: if there's pending zone creation/deletions, it launches them | |
81 | _cloudkitRetryAfter = [[CKKSNearFutureScheduler alloc] initWithName:@"zonemodifier-ckretryafter" | |
82 | initialDelay:100*NSEC_PER_MSEC | |
83 | continuingDelay:100*NSEC_PER_MSEC | |
84 | keepProcessAlive:false | |
85 | dependencyDescriptionCode:CKKSResultDescriptionPendingCloudKitRetryAfter | |
86 | block:^{ | |
87 | STRONGIFY(self); | |
88 | [self launchOperations]; | |
89 | }]; | |
90 | ||
91 | _ckOperations = [NSHashTable weakObjectsHashTable]; | |
92 | } | |
93 | return self; | |
94 | } | |
95 | ||
96 | - (void)_onqueueCreatePendingObjects | |
97 | { | |
98 | dispatch_assert_queue(self.queue); | |
99 | ||
100 | if(!self.pendingOperations) { | |
101 | CKKSResultOperation* zoneModificationOperationDependency = [CKKSResultOperation named:@"zone-modification" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) { | |
102 | }]; | |
103 | ||
104 | CKKSResultOperation* zoneSubscriptionOperationDependency = [CKKSResultOperation named:@"zone-subscription" withBlockTakingSelf:^(CKKSResultOperation * _Nonnull op) { | |
105 | }]; | |
106 | ||
107 | self.pendingOperations = [[CKKSZoneModifyOperations alloc] initWithZoneModificationOperation:zoneModificationOperationDependency | |
108 | zoneSubscriptionOperation:zoneSubscriptionOperationDependency]; | |
109 | } | |
110 | } | |
111 | ||
112 | - (CKKSZoneModifyOperations*)createZone:(CKRecordZone*)zone | |
113 | { | |
114 | __block CKKSZoneModifyOperations* ops = nil; | |
115 | ||
116 | dispatch_sync(self.queue, ^{ | |
117 | [self _onqueueCreatePendingObjects]; | |
118 | ops = self.pendingOperations; | |
119 | ||
120 | [ops.zonesToCreate addObject:zone]; | |
121 | CKRecordZoneSubscription* subscription = [[CKRecordZoneSubscription alloc] initWithZoneID:zone.zoneID | |
122 | subscriptionID:[@"zone:" stringByAppendingString: zone.zoneID.zoneName]]; | |
123 | [ops.subscriptionsToSubscribe addObject:subscription]; | |
124 | }); | |
125 | ||
126 | [self.cloudkitRetryAfter trigger]; | |
127 | ||
128 | return ops; | |
129 | } | |
130 | ||
131 | - (CKKSZoneModifyOperations*)deleteZone:(CKRecordZoneID*)zoneID | |
132 | { | |
133 | __block CKKSZoneModifyOperations* ops = nil; | |
134 | ||
135 | dispatch_sync(self.queue, ^{ | |
136 | [self _onqueueCreatePendingObjects]; | |
137 | ops = self.pendingOperations; | |
138 | ||
139 | [ops.zoneIDsToDelete addObject:zoneID]; | |
140 | }); | |
141 | ||
142 | [self.cloudkitRetryAfter trigger]; | |
143 | ||
144 | return ops; | |
145 | } | |
146 | ||
147 | - (void)halt | |
148 | { | |
149 | dispatch_sync(self.queue, ^{ | |
150 | self.halted = true; | |
151 | }); | |
152 | } | |
153 | ||
154 | // Called by the NearFutureScheduler | |
155 | - (void)launchOperations | |
156 | { | |
157 | dispatch_sync(self.queue, ^{ | |
158 | if(self.halted) { | |
159 | secnotice("ckkszonemodifier", "Halted; not launching operations"); | |
160 | return; | |
161 | } | |
162 | ||
163 | CKKSZoneModifyOperations* ops = self.pendingOperations; | |
164 | if(!ops) { | |
165 | secinfo("ckkszonemodifier", "No pending zone modification operations; quitting"); | |
166 | return; | |
167 | } | |
168 | ||
169 | if(self.inflightOperations && (![self.inflightOperations.zoneModificationOperation isFinished] || | |
170 | ![self.inflightOperations.zoneSubscriptionOperation isFinished])) { | |
171 | secnotice("ckkszonemodifier", "Have in-flight zone modification operations, will retry later"); | |
172 | ||
173 | WEAKIFY(self); | |
174 | CKKSResultOperation* retrigger = [CKKSResultOperation named:@"retry" withBlock:^{ | |
175 | STRONGIFY(self); | |
176 | [self.cloudkitRetryAfter trigger]; | |
177 | }]; | |
178 | [retrigger addNullableDependency:self.inflightOperations.zoneModificationOperation]; | |
179 | [retrigger addNullableDependency:self.inflightOperations.zoneSubscriptionOperation]; | |
180 | [self.operationQueue addOperation:retrigger]; | |
181 | return; | |
182 | } | |
183 | ||
184 | self.pendingOperations = nil; | |
185 | self.inflightOperations = ops; | |
186 | ||
187 | CKDatabaseOperation<CKKSModifyRecordZonesOperation>* modifyZonesOperation = [self createModifyZonesOperation:ops]; | |
188 | CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [self createModifySubscriptionsOperation:ops]; | |
189 | ||
190 | [self.operationQueue addOperation:modifyZonesOperation]; | |
191 | if(zoneSubscriptionOperation) { | |
192 | [self.operationQueue addOperation:zoneSubscriptionOperation]; | |
193 | } | |
194 | }); | |
195 | } | |
196 | ||
197 | - (CKDatabaseOperation<CKKSModifyRecordZonesOperation>*)createModifyZonesOperation:(CKKSZoneModifyOperations*)ops | |
198 | { | |
199 | secnotice("ckkszonemodifier", "Attempting to create zones %@, delete zones %@", ops.zonesToCreate, ops.zoneIDsToDelete); | |
200 | ||
201 | CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneModifyOperation = [[self.cloudKitClassDependencies.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave:ops.zonesToCreate recordZoneIDsToDelete:ops.zoneIDsToDelete]; | |
202 | [zoneModifyOperation linearDependencies:self.ckOperations]; | |
203 | ||
204 | zoneModifyOperation.configuration.automaticallyRetryNetworkFailures = NO; | |
205 | zoneModifyOperation.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary; | |
206 | zoneModifyOperation.configuration.isCloudKitSupportOperation = YES; | |
207 | zoneModifyOperation.database = self.database; | |
208 | zoneModifyOperation.name = @"zone-creation-operation"; | |
209 | zoneModifyOperation.group = [CKOperationGroup CKKSGroupWithName:@"zone-creation"];; | |
210 | ||
211 | // We will use the zoneCreationOperation operation in ops to signal completion | |
212 | WEAKIFY(self); | |
213 | ||
214 | zoneModifyOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, | |
215 | NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, | |
216 | NSError *operationError) { | |
217 | STRONGIFY(self); | |
218 | ||
219 | if(operationError) { | |
220 | secerror("ckkszonemodifier: Zone modification failed: %@", operationError); | |
221 | [self inspectErrorForRetryAfter:operationError]; | |
222 | ||
223 | if ([self.reachabilityTracker isNetworkError:operationError]){ | |
224 | self.networkFailure = true; | |
225 | } | |
226 | } | |
227 | secnotice("ckkszonemodifier", "created zones: %@", savedRecordZones); | |
228 | secnotice("ckkszonemodifier", "deleted zones: %@", deletedRecordZoneIDs); | |
229 | ||
230 | ops.savedRecordZones = savedRecordZones; | |
231 | ops.deletedRecordZoneIDs = deletedRecordZoneIDs; | |
232 | ops.zoneModificationOperation.error = operationError; | |
233 | ||
234 | [self.operationQueue addOperation:ops.zoneModificationOperation]; | |
235 | }; | |
236 | ||
237 | if(self.networkFailure) { | |
238 | secnotice("ckkszonemodifier", "Waiting for reachabilty before issuing zone creation"); | |
239 | [zoneModifyOperation addNullableDependency:self.reachabilityTracker.reachabilityDependency]; | |
240 | } | |
241 | ||
242 | return zoneModifyOperation; | |
243 | } | |
244 | ||
245 | - (CKDatabaseOperation<CKKSModifySubscriptionsOperation>* _Nullable)createModifySubscriptionsOperation:(CKKSZoneModifyOperations*)ops | |
246 | { | |
247 | if(ops.subscriptionsToSubscribe.count == 0) { | |
248 | [self.operationQueue addOperation: ops.zoneSubscriptionOperation]; | |
249 | return nil; | |
250 | } | |
251 | ||
252 | CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[self.cloudKitClassDependencies.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave:ops.subscriptionsToSubscribe subscriptionIDsToDelete:nil]; | |
253 | [zoneSubscriptionOperation linearDependencies:self.ckOperations]; | |
254 | ||
255 | zoneSubscriptionOperation.configuration.automaticallyRetryNetworkFailures = NO; | |
256 | zoneSubscriptionOperation.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary; | |
257 | zoneSubscriptionOperation.configuration.isCloudKitSupportOperation = YES; | |
258 | zoneSubscriptionOperation.database = self.database; | |
259 | zoneSubscriptionOperation.name = @"zone-subscription-operation"; | |
260 | ||
261 | // Completion blocks don't count for dependencies. Use this intermediate operation hack instead. | |
262 | NSBlockOperation* zoneSubscriptionCompleteOperation = [[NSBlockOperation alloc] init]; | |
263 | zoneSubscriptionCompleteOperation.name = @"zone-subscription-complete"; | |
264 | ||
265 | WEAKIFY(self); | |
266 | zoneSubscriptionOperation.modifySubscriptionsCompletionBlock = ^(NSArray<CKSubscription *> * _Nullable savedSubscriptions, | |
267 | NSArray<NSString *> * _Nullable deletedSubscriptionIDs, | |
268 | NSError * _Nullable operationError) { | |
269 | STRONGIFY(self); | |
270 | ||
271 | if(operationError) { | |
272 | secerror("ckkszonemodifier: Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError); | |
273 | [self inspectErrorForRetryAfter:operationError]; | |
274 | ||
275 | if ([self.reachabilityTracker isNetworkError:operationError]){ | |
276 | self.networkFailure = true; | |
277 | } | |
278 | } | |
279 | secnotice("ckkszonemodifier", "Successfully subscribed to %@", savedSubscriptions); | |
280 | ||
281 | ops.savedSubscriptions = savedSubscriptions; | |
282 | ops.deletedSubscriptionIDs = deletedSubscriptionIDs; | |
283 | ops.zoneSubscriptionOperation.error = operationError; | |
284 | ||
285 | [self.operationQueue addOperation: ops.zoneSubscriptionOperation]; | |
286 | }; | |
287 | ||
288 | [zoneSubscriptionOperation addNullableDependency:ops.zoneModificationOperation]; | |
289 | ||
290 | if(self.networkFailure) { | |
291 | secnotice("ckkszonemodifier", "Waiting for reachabilty before issuing zone subscription"); | |
292 | [zoneSubscriptionOperation addNullableDependency:self.reachabilityTracker.reachabilityDependency]; | |
293 | } | |
294 | ||
295 | return zoneSubscriptionOperation; | |
296 | } | |
297 | ||
298 | - (void)inspectErrorForRetryAfter:(NSError*)ckerror | |
299 | { | |
300 | NSTimeInterval delay = CKRetryAfterSecondsForError(ckerror); | |
301 | if(delay) { | |
302 | uint64_t ns_delay = NSEC_PER_SEC * ((uint64_t) delay); | |
303 | secnotice("ckkszonemodifier", "CK operation failed with rate-limit, scheduling delay for %.1f seconds: %@", delay, ckerror); | |
304 | [self.cloudkitRetryAfter waitUntil:ns_delay]; | |
305 | } | |
306 | } | |
307 | ||
308 | @end | |
309 | ||
310 | #endif // OCTAGON |