]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSZone.m
Security-58286.31.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSZone.m
1 /*
2 * Copyright (c) 2016 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 #include <AssertMacros.h>
25
26 #import <Foundation/Foundation.h>
27
28 #if OCTAGON
29 #import "CloudKitDependencies.h"
30 #import "keychain/ckks/CKKSCKAccountStateTracker.h"
31 #import <CloudKit/CloudKit.h>
32 #import <CloudKit/CloudKit_Private.h>
33 #endif
34
35 #import "CKKSKeychainView.h"
36 #import "CKKSZone.h"
37
38 #include <utilities/debugging.h>
39
40 @interface CKKSZone()
41 #if OCTAGON
42
43 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
44 @property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
45 @property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
46
47 @property bool acceptingNewOperations;
48 @property NSOperationQueue* operationQueue;
49 @property NSOperation* accountLoggedInDependency;
50
51 @property NSHashTable<NSOperation*>* accountOperations;
52 #endif
53 @end
54
55 @implementation CKKSZone
56
57 #if OCTAGON
58
59 - (instancetype)initWithContainer: (CKContainer*) container
60 zoneName: (NSString*) zoneName
61 accountTracker:(CKKSCKAccountStateTracker*) tracker
62 fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
63 fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
64 queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
65 modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
66 modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
67 apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
68 {
69 if(self = [super init]) {
70 _container = container;
71 _zoneName = zoneName;
72 _accountTracker = tracker;
73
74 _database = [_container privateCloudDatabase];
75 _zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
76
77 // Every subclass must set up call beginSetup at least once.
78 _accountStatus = CKKSAccountStatusUnknown;
79 [self resetSetup];
80
81 _accountOperations = [NSHashTable weakObjectsHashTable];
82
83 _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
84 _fetchRecordsOperationClass = fetchRecordsOperationClass;
85 _queryOperationClass = queryOperationClass;
86 _modifySubscriptionsOperationClass = modifySubscriptionsOperationClass;
87 _modifyRecordZonesOperationClass = modifyRecordZonesOperationClass;
88 _apsConnectionClass = apsConnectionClass;
89
90 _queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL);
91 _operationQueue = [[NSOperationQueue alloc] init];
92 _acceptingNewOperations = true;
93 }
94 return self;
95 }
96
97 // Initialize this object so that we can call beginSetup again
98 - (void)resetSetup {
99 self.setupStarted = false;
100 self.setupComplete = false;
101
102 if([self.zoneSetupOperation isPending]) {
103 // Nothing to do here: there's already an existing zoneSetupOperation
104 } else {
105 self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
106 self.zoneSetupOperation.name = @"zone-setup-operation";
107 }
108
109 if([self.accountLoggedInDependency isPending]) {
110 // Nothing to do here: there's already an existing accountLoggedInDependency
111 } else {
112 __weak __typeof(self) weakSelf = self;
113 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
114 ckksnotice("ckkszone", weakSelf, "CloudKit account logged in.");
115 }];
116 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
117 }
118
119 self.zoneCreated = false;
120 self.zoneSubscribed = false;
121 self.zoneCreatedError = nil;
122 self.zoneSubscribedError = nil;
123
124 self.zoneCreationOperation = nil;
125 self.zoneSubscriptionOperation = nil;
126 self.zoneDeletionOperation = nil;
127 }
128
129 - (CKRecordZoneID*)zoneID {
130 return [self.zone zoneID];
131 }
132
133
134 -(void)ckAccountStatusChange: (CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
135
136 // dispatch this on a serial queue, so we get each transition in order
137 [self dispatchSync: ^bool {
138 ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
139 self.zoneID.zoneName,
140 [CKKSCKAccountStateTracker stringFromAccountStatus: self.accountStatus],
141 [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
142 CKKSAccountStatus oldStatus = self.accountStatus;
143 self.accountStatus = currentStatus;
144
145 switch(currentStatus) {
146 case CKKSAccountStatusAvailable: {
147
148 ckksinfo("ckkszone", self, "logging in while setup started: %d and complete: %d", self.setupStarted, self.setupComplete);
149
150 // This is only a login if we're not in the middle of setup, and the previous state was not logged in
151 if(!(self.setupStarted ^ self.setupComplete) && oldStatus != CKKSAccountStatusAvailable) {
152 [self resetSetup];
153 [self handleCKLogin];
154 }
155
156 if(self.accountLoggedInDependency) {
157 [self.operationQueue addOperation:self.accountLoggedInDependency];
158 self.accountLoggedInDependency = nil;
159 };
160 }
161 break;
162
163 case CKKSAccountStatusNoAccount: {
164 ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
165
166 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
167 ckksnotice("ckkszone", self, "CloudKit account logged in again.");
168 }];
169 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
170
171 [self.operationQueue cancelAllOperations];
172 [self handleCKLogout];
173
174 // now we're in a logged out state. Optimistically prepare for a log in!
175 [self resetSetup];
176 }
177 break;
178
179 case CKKSAccountStatusUnknown: {
180 // We really don't expect to receive this as a notification, but, okay!
181 ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
182
183 self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
184 ckksnotice("ckkszone", self, "CloudKit account restored from 'unknown'.");
185 }];
186 self.accountLoggedInDependency.name = @"account-logged-in-dependency";
187
188 [self.operationQueue cancelAllOperations];
189 [self resetSetup];
190 }
191 break;
192 }
193
194 return true;
195 }];
196 }
197
198 - (NSOperation*) createSetupOperation: (bool) zoneCreated zoneSubscribed: (bool) zoneSubscribed {
199 if(!SecCKKSIsEnabled()) {
200 ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
201 return nil;
202 }
203
204 // If we've already started set up, skip doing it again.
205 if(self.setupStarted) {
206 ckksinfo("ckkszone", self, "skipping startup: it's already started");
207 return self.zoneSetupOperation;
208 }
209
210 if(self.zoneSetupOperation == nil) {
211 ckkserror("ckkszone", self, "trying to set up but the setup operation is gone; what happened?");
212 return nil;
213 }
214
215 self.zoneCreated = zoneCreated;
216 self.zoneSubscribed = zoneSubscribed;
217
218 // Zone setups and teardowns are due to either 1) first CKKS launch or 2) the user logging in to iCloud.
219 // Therefore, they're QoS UserInitiated.
220 self.zoneSetupOperation.queuePriority = NSOperationQueuePriorityNormal;
221 self.zoneSetupOperation.qualityOfService = NSQualityOfServiceUserInitiated;
222
223 ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
224 self.setupStarted = true;
225
226 __weak __typeof(self) weakSelf = self;
227
228 // First, check the account status. If it's sufficient, add the necessary CloudKit operations to this operation
229 NSBlockOperation* doSetup = [NSBlockOperation blockOperationWithBlock:^{
230 __strong __typeof(weakSelf) strongSelf = weakSelf;
231 if(!strongSelf) {
232 ckkserror("ckkszone", strongSelf, "received callback for released object");
233 return;
234 }
235
236 __block bool ret = false;
237 [strongSelf dispatchSync: ^bool {
238 strongSelf.accountStatus = [strongSelf.accountTracker currentCKAccountStatusAndNotifyOnChange:strongSelf];
239
240 switch(strongSelf.accountStatus) {
241 case CKKSAccountStatusNoAccount:
242 ckkserror("ckkszone", strongSelf, "No CloudKit account; quitting setup for %@", strongSelf.zoneID.zoneName);
243 [strongSelf handleCKLogout];
244 ret = true;
245 break;
246 case CKKSAccountStatusAvailable:
247 if(strongSelf.accountLoggedInDependency) {
248 [strongSelf.operationQueue addOperation: strongSelf.accountLoggedInDependency];
249 strongSelf.accountLoggedInDependency = nil;
250 }
251 break;
252 case CKKSAccountStatusUnknown:
253 ckkserror("ckkszone", strongSelf, "CloudKit account status currently unknown; stopping setup for %@", strongSelf.zoneID.zoneName);
254 ret = true;
255 break;
256 }
257
258 return true;
259 }];
260
261 NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
262 __strong __typeof(weakSelf) strongSelf = weakSelf;
263 if(!strongSelf) {
264 secerror("ckkszone: received callback for released object");
265 return;
266 }
267
268 ckksinfo("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
269 strongSelf.setupComplete = true;
270 }];
271 setupCompleteOperation.name = @"zone-setup-complete-operation";
272
273 // If we don't have an CloudKit account, don't bother continuing
274 if(ret) {
275 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
276 return;
277 }
278
279 // We have an account, so fetch the push environment and bring up APS
280 [strongSelf.container serverPreferredPushEnvironmentWithCompletionHandler: ^(NSString *apsPushEnvString, NSError *error) {
281 __strong __typeof(weakSelf) strongSelf = weakSelf;
282 if(!strongSelf) {
283 secerror("ckkszone: received callback for released object");
284 return;
285 }
286
287 if(error || (apsPushEnvString == nil)) {
288 ckkserror("ckkszone", strongSelf, "Received error fetching preferred push environment (%@). Keychain syncing is highly degraded: %@", apsPushEnvString, error);
289 } else {
290 CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString
291 namedDelegatePort:SecCKKSAPSNamedPort
292 apsConnectionClass:strongSelf.apsConnectionClass];
293 [aps register:strongSelf forZoneID:strongSelf.zoneID];
294 }
295 }];
296
297 NSBlockOperation* modifyRecordZonesCompleteOperation = nil;
298 if(!zoneCreated) {
299 ckksnotice("ckkszone", strongSelf, "Creating CloudKit zone '%@'", strongSelf.zoneName);
300 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation = [[strongSelf.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: @[strongSelf.zone] recordZoneIDsToDelete: nil];
301 zoneCreationOperation.queuePriority = NSOperationQueuePriorityNormal;
302 zoneCreationOperation.qualityOfService = NSQualityOfServiceUserInitiated;
303 zoneCreationOperation.database = strongSelf.database;
304 zoneCreationOperation.name = @"zone-creation-operation";
305
306 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
307 modifyRecordZonesCompleteOperation = [[NSBlockOperation alloc] init];
308 modifyRecordZonesCompleteOperation.name = @"zone-creation-finished";
309
310 zoneCreationOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
311 __strong __typeof(weakSelf) strongSelf = weakSelf;
312 if(!strongSelf) {
313 secerror("ckkszone: received callback for released object");
314 return;
315 }
316
317 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
318
319 if(!operationError) {
320 ckksnotice("ckkszone", strongSubSelf, "Successfully created zone %@", strongSubSelf.zoneName);
321 strongSubSelf.zoneCreated = true;
322 } else {
323 ckkserror("ckkszone", strongSubSelf, "Couldn't create zone %@; %@", strongSubSelf.zoneName, operationError);
324 }
325 strongSubSelf.zoneCreatedError = operationError;
326
327 [strongSubSelf.operationQueue addOperation: modifyRecordZonesCompleteOperation];
328 };
329
330 ckksnotice("ckkszone", strongSelf, "Adding CKKSModifyRecordZonesOperation: %@ %@", zoneCreationOperation, zoneCreationOperation.dependencies);
331 strongSelf.zoneCreationOperation = zoneCreationOperation;
332 [setupCompleteOperation addDependency: modifyRecordZonesCompleteOperation];
333 [strongSelf.zoneSetupOperation runBeforeGroupFinished: zoneCreationOperation];
334 [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: modifyRecordZonesCompleteOperation];
335 } else {
336 ckksinfo("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
337 }
338
339 if(!zoneSubscribed) {
340 ckksnotice("ckkszone", strongSelf, "Creating CloudKit record zone subscription for %@", strongSelf.zoneName);
341 CKRecordZoneSubscription* subscription = [[CKRecordZoneSubscription alloc] initWithZoneID: strongSelf.zoneID subscriptionID:[@"zone:" stringByAppendingString: strongSelf.zoneName]];
342 CKNotificationInfo* notificationInfo = [[CKNotificationInfo alloc] init];
343
344 notificationInfo.shouldSendContentAvailable = false;
345 subscription.notificationInfo = notificationInfo;
346
347 CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation = [[strongSelf.modifySubscriptionsOperationClass alloc] initWithSubscriptionsToSave: @[subscription] subscriptionIDsToDelete: nil];
348
349 zoneSubscriptionOperation.queuePriority = NSOperationQueuePriorityNormal;
350 zoneSubscriptionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
351 zoneSubscriptionOperation.database = strongSelf.database;
352 zoneSubscriptionOperation.name = @"zone-subscription-operation";
353
354 // Completion blocks don't count for dependencies. Use this intermediate operation hack instead.
355 NSBlockOperation* zoneSubscriptionCompleteOperation = [[NSBlockOperation alloc] init];
356 zoneSubscriptionCompleteOperation.name = @"zone-subscription-complete";
357 zoneSubscriptionOperation.modifySubscriptionsCompletionBlock = ^(NSArray<CKSubscription *> * _Nullable savedSubscriptions, NSArray<NSString *> * _Nullable deletedSubscriptionIDs, NSError * _Nullable operationError) {
358 __strong __typeof(weakSelf) strongSubSelf = weakSelf;
359 if(!strongSubSelf) {
360 ckkserror("ckkszone", strongSubSelf, "received callback for released object");
361 return;
362 }
363
364 if(!operationError) {
365 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", savedSubscriptions);
366
367 // Success; write that down. TODO: actually ensure that the saved subscription matches what we asked for
368 for(CKSubscription* sub in savedSubscriptions) {
369 ckksnotice("ckkszone", strongSubSelf, "Successfully subscribed to %@", sub.subscriptionID);
370 strongSubSelf.zoneSubscribed = true;
371 }
372 } else {
373 ckkserror("ckkszone", strongSubSelf, "Couldn't create cloudkit zone subscription; keychain syncing is severely degraded: %@", operationError);
374 }
375
376 strongSubSelf.zoneSubscribedError = operationError;
377 strongSubSelf.zoneSubscriptionOperation = nil;
378
379 [strongSubSelf.operationQueue addOperation: zoneSubscriptionCompleteOperation];
380 };
381
382 if(modifyRecordZonesCompleteOperation) {
383 [zoneSubscriptionOperation addDependency:modifyRecordZonesCompleteOperation];
384 }
385 strongSelf.zoneSubscriptionOperation = zoneSubscriptionOperation;
386 [setupCompleteOperation addDependency: zoneSubscriptionCompleteOperation];
387 [strongSelf.zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
388 [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
389 } else {
390 ckksinfo("ckkszone", strongSelf, "no need to create database subscription");
391 }
392
393 [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
394 }];
395 doSetup.name = @"begin-zone-setup";
396
397 [self.zoneSetupOperation runBeforeGroupFinished:doSetup];
398
399 return self.zoneSetupOperation;
400 }
401
402
403 - (CKKSResultOperation*)beginResetCloudKitZoneOperation {
404 if(!SecCKKSIsEnabled()) {
405 ckksinfo("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
406 return nil;
407 }
408
409 // We want to delete this zone and this subscription from CloudKit.
410
411 // Step 1: cancel setup operations (if they exist)
412 [self.accountLoggedInDependency cancel];
413 [self.zoneSetupOperation cancel];
414 [self.zoneCreationOperation cancel];
415 [self.zoneSubscriptionOperation cancel];
416
417 // Step 2: Try to delete the zone
418 CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation = [[self.modifyRecordZonesOperationClass alloc] initWithRecordZonesToSave: nil recordZoneIDsToDelete: @[self.zoneID]];
419 zoneDeletionOperation.queuePriority = NSOperationQueuePriorityNormal;
420 zoneDeletionOperation.qualityOfService = NSQualityOfServiceUserInitiated;
421 zoneDeletionOperation.database = self.database;
422
423 CKKSResultOperation* doneOp = [CKKSResultOperation named:@"zone-reset-watcher" withBlock:^{}];
424
425 __weak __typeof(self) weakSelf = self;
426
427 zoneDeletionOperation.modifyRecordZonesCompletionBlock = ^(NSArray<CKRecordZone *> *savedRecordZones, NSArray<CKRecordZoneID *> *deletedRecordZoneIDs, NSError *operationError) {
428 __strong __typeof(weakSelf) strongSelf = weakSelf;
429 if(!strongSelf) {
430 ckkserror("ckkszone", strongSelf, "received callback for released object");
431 return;
432 }
433
434 bool fatalError = false;
435 if(operationError) {
436 // Okay, but if this error is either 'ZoneNotFound' or 'UserDeletedZone', that's fine by us: the zone is deleted.
437 NSDictionary* partialErrors = operationError.userInfo[CKPartialErrorsByItemIDKey];
438 if([operationError.domain isEqualToString:CKErrorDomain] && operationError.code == CKErrorPartialFailure && partialErrors) {
439 for(CKRecordZoneID* errorZoneID in partialErrors.allKeys) {
440 NSError* errorZone = partialErrors[errorZoneID];
441
442 if(errorZone && [errorZone.domain isEqualToString:CKErrorDomain] &&
443 (errorZone.code == CKErrorZoneNotFound || errorZone.code == CKErrorUserDeletedZone)) {
444 ckksnotice("ckkszone", strongSelf, "Attempted to delete zone %@, but it's already missing. This is okay: %@", errorZoneID, errorZone);
445 } else {
446 fatalError = true;
447 }
448 }
449
450 } else {
451 fatalError = true;
452 }
453 }
454
455 ckksinfo("ckkszone", strongSelf, "record zones deletion %@ completed with error: %@", deletedRecordZoneIDs, operationError);
456 [strongSelf resetSetup];
457
458 if(operationError && fatalError) {
459 // If the error wasn't actually a problem, don't report it upward.
460 doneOp.error = operationError;
461 }
462 [strongSelf.operationQueue addOperation: doneOp];
463 };
464
465 // If the zone creation operation is still pending, wait for it to complete before attempting zone deletion
466 [zoneDeletionOperation addNullableDependency: self.zoneCreationOperation];
467
468 ckksinfo("ckkszone", self, "deleting zone with %@ %@", zoneDeletionOperation, zoneDeletionOperation.dependencies);
469 // Don't use scheduleOperation: zone deletions should be attempted even if we're "logged out"
470 [self.operationQueue addOperation: zoneDeletionOperation];
471 self.zoneDeletionOperation = zoneDeletionOperation;
472 return doneOp;
473 }
474
475 - (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
476 ckksnotice("ckkszone", self, "received a notification for CK zone change, ignoring");
477 }
478
479 - (void)handleCKLogin {
480 ckksinfo("ckkszone", self, "received a notification of CK login, ignoring");
481 }
482
483 - (void)handleCKLogout {
484 ckksinfo("ckkszone", self, "received a notification of CK logout, ignoring");
485 }
486
487 - (bool)scheduleOperation: (NSOperation*) op {
488 if(!self.acceptingNewOperations) {
489 ckksdebug("ckkszone", self, "attempted to schedule an operation on a cancelled zone, ignoring");
490 return false;
491 }
492
493 if(self.accountLoggedInDependency) {
494 [op addDependency: self.accountLoggedInDependency];
495 }
496
497 [self.operationQueue addOperation: op];
498 return true;
499 }
500
501 - (void)cancelAllOperations {
502 [self.operationQueue cancelAllOperations];
503 }
504
505 - (void)waitUntilAllOperationsAreFinished {
506 [self.operationQueue waitUntilAllOperationsAreFinished];
507 }
508
509 - (void)waitForOperationsOfClass:(Class) operationClass {
510 NSArray* operations = [self.operationQueue.operations copy];
511 for(NSOperation* op in operations) {
512 if([op isKindOfClass:operationClass]) {
513 [op waitUntilFinished];
514 }
515 }
516 }
517
518 - (bool)scheduleAccountStatusOperation: (NSOperation*) op {
519 // Always succeed. But, account status operations should always proceed in-order.
520 [op linearDependencies:self.accountOperations];
521 [self.operationQueue addOperation: op];
522 return true;
523 }
524
525 // to be used rarely, if at all
526 - (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
527 [self.operationQueue addOperation: op];
528 return true;
529 }
530
531 - (void) dispatchSync: (bool (^)(void)) block {
532 // important enough to block this thread.
533 __block bool ok = false;
534 dispatch_sync(self.queue, ^{
535 ok = block();
536 if(!ok) {
537 ckkserror("ckkszone", self, "CKKSZone block returned false");
538 }
539 });
540 }
541
542
543 #endif /* OCTAGON */
544 @end
545
546
547