]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSGroupOperation.m
443ef471c817192ba4517674c6b87e0e0547fc6f
[apple/security.git] / keychain / ckks / CKKSGroupOperation.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 #import "CKKSGroupOperation.h"
25 #import "CKKSCondition.h"
26 #include <utilities/debugging.h>
27
28 @implementation NSOperation (CKKSUsefulPrintingOperation)
29 - (NSString*)selfname {
30 if(self.name) {
31 return [NSString stringWithFormat: @"%@(%@)", NSStringFromClass([self class]), self.name];
32 } else {
33 return NSStringFromClass([self class]);
34 }
35 }
36
37 -(void)linearDependencies: (NSHashTable*) collection {
38 @synchronized(collection) {
39 for(NSOperation* existingop in collection) {
40 if(existingop == self) {
41 // don't depend on yourself
42 continue;
43 }
44 [self addDependency: existingop];
45 }
46 [collection addObject:self];
47 }
48 }
49
50 -(void)linearDependenciesWithSelfFirst: (NSHashTable*) collection {
51 @synchronized(collection) {
52 for(NSOperation* existingop in collection) {
53 if(existingop == self) {
54 // don't depend on yourself
55 continue;
56 }
57
58 if([existingop isPending]) {
59 [existingop addDependency: self];
60 if([existingop isPending]) {
61 // Good, we're ahead of this one.
62 } else {
63 // It started before we told it to wait on us. Reverse the dependency.
64 [existingop removeDependency: self];
65 [self addDependency:existingop];
66 }
67 } else {
68 // Not a pending op? We depend on it.
69 [self addDependency: existingop];
70 }
71 }
72 [collection addObject:self];
73 }
74 }
75
76 -(NSString*)pendingDependenciesString:(NSString*)prefix {
77 NSArray* dependencies = [self.dependencies copy];
78 dependencies = [dependencies objectsAtIndexes: [dependencies indexesOfObjectsPassingTest: ^BOOL (id obj,
79 NSUInteger idx,
80 BOOL* stop) {
81 return [obj isPending] ? YES : NO;
82 }]];
83
84 if(dependencies.count == 0u) {
85 return @"";
86 }
87
88 return [NSString stringWithFormat: @"%@%@", prefix, [dependencies componentsJoinedByString: @", "]];
89 }
90
91 - (NSString*)description {
92 NSString* state = ([self isFinished] ? @"finished" :
93 [self isCancelled] ? @"cancelled" :
94 [self isExecuting] ? @"executing" :
95 [self isReady] ? @"ready" :
96 @"pending");
97
98 return [NSString stringWithFormat: @"<%@: %@%@>", [self selfname], state, [self pendingDependenciesString: @" dep:"]];
99 }
100 - (NSString*)debugDescription {
101 NSString* state = ([self isFinished] ? @"finished" :
102 [self isCancelled] ? @"cancelled" :
103 [self isExecuting] ? @"executing" :
104 [self isReady] ? @"ready" :
105 @"pending");
106
107 return [NSString stringWithFormat: @"<%@ (%p): %@%@>", [self selfname], self, state, [self pendingDependenciesString: @" dep:"]];
108 }
109
110 - (BOOL)isPending {
111 return (!([self isExecuting] || [self isFinished])) ? YES : NO;
112 }
113
114 - (void)addNullableDependency: (NSOperation*) op {
115 if(op) {
116 [self addDependency:op];
117 }
118 }
119 @end
120
121 @implementation NSBlockOperation (CKKSUsefulConstructorOperation)
122 +(instancetype)named: (NSString*)name withBlock: (void(^)(void)) block {
123 // How many blocks could a block block if a block could block blocks?
124 NSBlockOperation* blockOp = [NSBlockOperation blockOperationWithBlock: block];
125 blockOp.name = name;
126 return blockOp;
127 }
128 @end
129
130
131 @interface CKKSResultOperation()
132 @property NSMutableArray<CKKSResultOperation*>* successDependencies;
133 @property bool timeoutCanOccur;
134 @property dispatch_queue_t timeoutQueue;
135 @property void (^finishingBlock)(void);
136 @end
137
138 @implementation CKKSResultOperation
139 - (instancetype)init {
140 if(self = [super init]) {
141 _error = nil;
142 _successDependencies = [[NSMutableArray alloc] init];
143 _timeoutCanOccur = true;
144 _timeoutQueue = dispatch_queue_create("result-operation-timeout", DISPATCH_QUEUE_SERIAL);
145 _completionHandlerDidRunCondition = [[CKKSCondition alloc] init];
146
147 __weak __typeof(self) weakSelf = self;
148 _finishingBlock = ^(void) {
149 weakSelf.finishDate = [NSDate dateWithTimeIntervalSinceNow:0];
150 };
151 self.completionBlock = ^{}; // our _finishing block gets added in the method override
152 }
153 return self;
154 }
155
156 - (NSString*)description {
157 NSString* state = ([self isFinished] ? [NSString stringWithFormat:@"finished %@", self.finishDate] :
158 [self isCancelled] ? @"cancelled" :
159 [self isExecuting] ? @"executing" :
160 [self isReady] ? @"ready" :
161 @"pending");
162
163 if(self.error) {
164 return [NSString stringWithFormat: @"<%@: %@ error:%@>", [self selfname], state, self.error];
165 } else {
166 return [NSString stringWithFormat: @"<%@: %@%@>", [self selfname], state, [self pendingDependenciesString:@" dep:"]];
167 }
168 }
169
170 - (NSString*)debugDescription {
171 return [self description];
172 }
173
174 - (void)setCompletionBlock:(void (^)(void))completionBlock
175 {
176 __weak __typeof(self) weakSelf = self;
177 [super setCompletionBlock:^(void) {
178 __strong __typeof(self) strongSelf = weakSelf;
179 if (!strongSelf) {
180 secerror("ckksresultoperation: completion handler called on deallocated operation instance");
181 completionBlock(); // go ahead and still behave as things would if this method override were not here
182 return;
183 }
184
185 strongSelf.finishingBlock();
186 completionBlock();
187 [strongSelf.completionHandlerDidRunCondition fulfill];
188 }];
189 }
190
191 - (void)start {
192 if(![self allDependentsSuccessful]) {
193 secdebug("ckksresultoperation", "Not running due to some failed dependent: %@", self.error);
194 [self cancel];
195 } else {
196 dispatch_sync(self.timeoutQueue, ^{
197 if(![self isCancelled]) {
198 self.timeoutCanOccur = false;
199 };
200 });
201 }
202
203 [super start];
204 }
205
206 - (instancetype)timeout:(dispatch_time_t)timeout {
207 __weak __typeof(self) weakSelf = self;
208 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.timeoutQueue, ^{
209 __strong __typeof(self) strongSelf = weakSelf;
210 if(strongSelf.timeoutCanOccur) {
211 strongSelf.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultTimedOut userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Operation timed out waiting to start for [%@]", [self pendingDependenciesString:@""]]}];
212 strongSelf.timeoutCanOccur = false;
213 [strongSelf cancel];
214 }
215 });
216
217 return self;
218 }
219
220 - (void)addSuccessDependency: (CKKSResultOperation*) operation {
221 if(!operation) {
222 return;
223 }
224 @synchronized(self) {
225 [self.successDependencies addObject: operation];
226 [self addDependency: operation];
227 }
228 }
229
230 - (bool)allDependentsSuccessful {
231 return [self allSuccessful: self.successDependencies];
232 }
233
234 - (bool)allSuccessful: (NSArray<CKKSResultOperation*>*) operations {
235 @synchronized(self) {
236 bool result = false;
237
238 bool finished = true; // all dependents must be finished
239 bool cancelled = false; // no dependents can be cancelled
240 bool failed = false; // no dependents can have failed
241
242 for(CKKSResultOperation* op in operations) {
243 finished &= !!([op isFinished]);
244 cancelled |= !!([op isCancelled]);
245 failed |= (op.error != nil);
246
247 // TODO: combine suberrors
248 if(op.error != nil) {
249 if([op.error.domain isEqual: CKKSResultErrorDomain] && op.error.code == CKKSResultSubresultError) {
250 // Already a subresult, just copy it on in
251 self.error = op.error;
252 } else {
253 self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultError userInfo:@{ NSUnderlyingErrorKey: op.error}];
254 }
255 }
256 }
257
258 result = finished && !( cancelled || failed );
259
260 if(!result && self.error == nil) {
261 self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultCancelled userInfo:nil];
262 }
263 return result;
264 }
265 }
266
267 + (CKKSResultOperation*)operationWithBlock:(void (^)(void))block {
268 CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
269 [op addExecutionBlock: block];
270 return op;
271 }
272
273 +(instancetype)named:(NSString*)name withBlock:(void(^)(void)) block {
274 CKKSResultOperation* blockOp = [CKKSResultOperation operationWithBlock: block];
275 blockOp.name = name;
276 return blockOp;
277 }
278 @end
279
280
281 @interface CKKSGroupOperation()
282 @property NSBlockOperation* startOperation;
283 @property NSBlockOperation* finishOperation;
284
285 @property NSMutableArray<CKKSResultOperation*>* internalSuccesses;
286 @end
287
288
289 @implementation CKKSGroupOperation
290
291 - (instancetype)init {
292 if(self = [super init]) {
293 __weak __typeof(self) weakSelf = self;
294
295 _operationQueue = [[NSOperationQueue alloc] init];
296 _internalSuccesses = [[NSMutableArray alloc] init];
297
298 // At start, we'll call this method (for subclasses)
299 _startOperation = [NSBlockOperation blockOperationWithBlock:^{
300 __strong __typeof(weakSelf) strongSelf = weakSelf;
301 if(!strongSelf) {
302 secerror("ckks: received callback for released object");
303 return;
304 }
305
306 if(![strongSelf allDependentsSuccessful]) {
307 secdebug("ckksgroup", "Not running due to some failed dependent: %@", strongSelf.error);
308 [strongSelf cancel];
309 return;
310 }
311
312 [strongSelf groupStart];
313 }];
314
315 // The finish operation will 'finish' us
316 _finishOperation = [NSBlockOperation blockOperationWithBlock:^{
317 __strong __typeof(weakSelf) strongSelf = weakSelf;
318 if(!strongSelf) {
319 secerror("ckks: received callback for released object");
320 return;
321 }
322
323 [strongSelf completeOperation];
324 }];
325
326 [self.finishOperation addDependency: self.startOperation];
327 [self.operationQueue addOperation: self.finishOperation];
328
329 self.startOperation.name = @"group-start";
330 self.finishOperation.name = @"group-finish";
331
332 executing = NO;
333 finished = NO;
334 }
335 return self;
336 }
337
338 - (void)dealloc {
339 // If the GroupOperation is dealloced before starting, all of its downstream operations form a retain loop.
340
341 if([self isPending]) {
342 [self.operationQueue cancelAllOperations];
343 [self.startOperation cancel];
344 [super cancel];
345 }
346 }
347
348 - (BOOL)isPending {
349 return [self.startOperation isPending];
350 }
351
352 - (void)setName:(NSString*) name {
353 self.operationQueue.name = [NSString stringWithFormat: @"group-queue:%@", name];
354 self.startOperation.name = [NSString stringWithFormat: @"group-start:%@", name];
355 self.finishOperation.name = [NSString stringWithFormat: @"group-finish:%@", name];
356 [super setName: name];
357 }
358
359 - (NSString*)description {
360 if(self.isFinished) {
361 if(self.error) {
362 return [NSString stringWithFormat: @"<%@: finished %@ - %@>", [self selfname], self.finishDate, self.error];
363 } else {
364 return [NSString stringWithFormat: @"<%@: finished %@>", [self selfname], self.finishDate];
365 }
366 }
367
368 NSMutableArray* ops = [self.operationQueue.operations mutableCopy];
369
370 [ops removeObject: self.finishOperation];
371
372 // Any extra dependencies from the finish operation should be considered part of this group
373 for(NSOperation* finishDep in self.finishOperation.dependencies) {
374 if(finishDep != self.startOperation && (NSNotFound == [ops indexOfObject: finishDep])) {
375 [ops addObject: finishDep];
376 }
377 }
378
379 NSString* opsString = [ops componentsJoinedByString:@", "];
380
381 if(self.error) {
382 return [NSString stringWithFormat: @"<%@: [%@] error:%@>", [self selfname], opsString, self.error];
383 } else {
384 return [NSString stringWithFormat: @"<%@: [%@]%@>", [self selfname], opsString, [self pendingDependenciesString:@" dep:"]];
385 }
386 }
387
388 - (NSString*)debugDescription {
389 return [self description];
390 }
391
392 - (BOOL)isConcurrent {
393 return YES;
394 }
395
396 - (BOOL)isExecuting {
397 return self->executing;
398 }
399
400 - (BOOL)isFinished {
401 return self->finished;
402 }
403
404 - (void)start {
405 if([self isCancelled]) {
406 [self willChangeValueForKey:@"isFinished"];
407 finished = YES;
408 [self didChangeValueForKey:@"isFinished"];
409 return;
410 }
411
412 [self.operationQueue addOperation: self.startOperation];
413
414 [self willChangeValueForKey:@"isExecuting"];
415 executing = YES;
416 [self didChangeValueForKey:@"isExecuting"];
417 }
418
419 - (void)cancel {
420 [self.operationQueue cancelAllOperations];
421 [self.startOperation cancel];
422
423 [super cancel];
424
425 // Our finishoperation might not fire (as we cancelled it above), so let's help it out
426 [self completeOperation];
427 }
428
429 - (void)completeOperation {
430 [self willChangeValueForKey:@"isFinished"];
431 [self willChangeValueForKey:@"isExecuting"];
432
433 // Run through all the failable operations in this group, and determine if we should be considered successful ourselves
434 [self allSuccessful: self.internalSuccesses];
435
436 executing = NO;
437 finished = YES;
438
439 [self didChangeValueForKey:@"isExecuting"];
440 [self didChangeValueForKey:@"isFinished"];
441 }
442
443 - (void)addDependency:(NSOperation *)op {
444 [super addDependency:op];
445 [self.startOperation addDependency: op];
446 }
447
448 - (void)groupStart {
449 // Do nothing. Subclasses can do things here.
450 }
451
452 - (void)runBeforeGroupFinished: (NSOperation*) suboperation {
453 if([self isCancelled]) {
454 // Cancelled operations can't add anything.
455 secnotice("ckksgroup", "Not adding operation to cancelled group");
456 return;
457 }
458
459 // op must wait for this operation to start
460 [suboperation addDependency: self.startOperation];
461
462 [self dependOnBeforeGroupFinished: suboperation];
463 [self.operationQueue addOperation: suboperation];
464 }
465
466 - (void)dependOnBeforeGroupFinished: (NSOperation*) suboperation {
467 if(suboperation == nil) {
468 return;
469 }
470
471 if([self isCancelled]) {
472 // Cancelled operations can't add anything.
473 secnotice("ckksgroup", "Can't add operation dependency to cancelled group");
474 return;
475 }
476
477 if([self.finishOperation isExecuting] || [self.finishOperation isFinished]) {
478 @throw @"Attempt to add operation to completed group";
479 }
480
481 // If this is a CKKSResultOperation, then its result impacts our result.
482 if([suboperation isKindOfClass: [CKKSResultOperation class]]) {
483 // don't use addSuccessDependency, because it's not a dependency for The Group Operation, but rather a suboperation
484 @synchronized(self) {
485 [self.internalSuccesses addObject: (CKKSResultOperation*) suboperation];
486 }
487 }
488
489 // Make sure it waits for us...
490 [suboperation addDependency: self.startOperation];
491 // and we wait for it.
492 [self.finishOperation addDependency: suboperation];
493 }
494
495 @end