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