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