2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import "keychain/ckks/CKKSResultOperation.h"
27 #import "keychain/ckks/NSOperationCategories.h"
28 #import "keychain/ckks/CKKSCondition.h"
29 #import "keychain/categories/NSError+UsefulConstructors.h"
30 #import "keychain/ot/ObjCImprovements.h"
31 #include <utilities/debugging.h>
33 @interface CKKSResultOperation()
34 @property NSMutableArray<CKKSResultOperation*>* successDependencies;
35 @property bool timeoutCanOccur;
36 @property dispatch_queue_t timeoutQueue;
37 @property void (^finishingBlock)(void);
40 @implementation CKKSResultOperation
41 - (instancetype)init {
42 if(self = [super init]) {
45 _successDependencies = [[NSMutableArray alloc] init];
46 _timeoutCanOccur = true;
47 _timeoutQueue = dispatch_queue_create("result-operation-timeout", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
48 _completionHandlerDidRunCondition = [[CKKSCondition alloc] init];
50 _finishingBlock = ^(void) {
52 self.finishDate = [NSDate dateWithTimeIntervalSinceNow:0];
54 self.completionBlock = ^{}; // our _finishing block gets added in the method override
59 - (NSString*)operationStateString {
60 return ([self isFinished] ? [NSString stringWithFormat:@"finished %@", self.finishDate] :
61 [self isCancelled] ? @"cancelled" :
62 [self isExecuting] ? @"executing" :
63 [self isReady] ? @"ready" :
68 - (NSString*)description {
69 static __thread unsigned __descriptionRecursion = 0;
70 NSString* state = [self operationStateString];
71 NSString *desc = NULL;
73 __descriptionRecursion++;
74 if(__descriptionRecursion > 10) {
75 desc = [NSString stringWithFormat: @"<%@: %@ recursion>", [self selfname], state];
76 } else if(self.error) {
77 desc = [NSString stringWithFormat: @"<%@: %@ error:%@>", [self selfname], state, self.error];
79 desc = [NSString stringWithFormat: @"<%@: %@%@>", [self selfname], state, [self pendingDependenciesString:@" dep:"]];
81 __descriptionRecursion--;
85 - (NSString*)debugDescription {
86 return [self description];
89 - (void)setCompletionBlock:(void (^)(void))completionBlock
92 [super setCompletionBlock:^(void) {
95 secerror("ckksresultoperation: completion handler called on deallocated operation instance");
96 completionBlock(); // go ahead and still behave as things would if this method override were not here
100 self.finishingBlock();
102 [self.completionHandlerDidRunCondition fulfill];
104 for (NSOperation *op in self.dependencies) {
105 [self removeDependency:op];
111 if(![self allDependentsSuccessful]) {
112 secdebug("ckksresultoperation", "Not running due to some failed dependent: %@", self.error);
115 [self invalidateTimeout];
122 - (void)invalidateTimeout {
123 dispatch_sync(self.timeoutQueue, ^{
124 if(![self isCancelled]) {
125 self.timeoutCanOccur = false;
130 - (NSError* _Nullable)dependenciesDescriptionError {
131 NSError* underlyingReason = nil;
132 NSArray* dependencies = [self.dependencies copy];
133 dependencies = [dependencies objectsAtIndexes: [dependencies indexesOfObjectsPassingTest: ^BOOL (id obj,
136 return [obj isFinished] ? NO : YES;
139 for(NSOperation* dependency in dependencies) {
140 if([dependency isKindOfClass:[CKKSResultOperation class]]) {
141 CKKSResultOperation* ro = (CKKSResultOperation*)dependency;
142 underlyingReason = [ro descriptionError] ?: underlyingReason;
146 return underlyingReason;
149 // Returns, for this CKKSResultOperation, an error describing this operation or its dependents.
150 // Used mainly by other CKKSResultOperations who time out waiting for this operation to start/complete.
151 - (NSError* _Nullable)descriptionError {
152 if(self.descriptionErrorCode != 0) {
153 return [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
154 code:self.descriptionErrorCode
157 return [self dependenciesDescriptionError];
161 - (NSError*)_onqueueTimeoutError {
162 // Find if any of our dependencies are CKKSResultOperations with a custom reason for existing
164 NSError* underlyingReason = [self descriptionError];
166 NSError* error = [NSError errorWithDomain:CKKSResultErrorDomain
167 code:CKKSResultTimedOut
168 description:[NSString stringWithFormat:@"Operation(%@) timed out waiting to start for [%@]",
170 [self pendingDependenciesString:@""]]
171 underlying:underlyingReason];
175 - (instancetype)timeout:(dispatch_time_t)timeout {
177 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.timeoutQueue, ^{
179 if(self.timeoutCanOccur) {
180 self.error = [self _onqueueTimeoutError];
181 self.timeoutCanOccur = false;
189 - (void)addSuccessDependency:(CKKSResultOperation *)operation {
190 [self addNullableSuccessDependency:operation];
193 - (void)addNullableSuccessDependency:(CKKSResultOperation *)operation {
197 @synchronized(self) {
198 [self.successDependencies addObject: operation];
199 [self addDependency: operation];
203 - (bool)allDependentsSuccessful {
204 return [self allSuccessful: self.successDependencies];
207 - (bool)allSuccessful: (NSArray<CKKSResultOperation*>*) operations {
208 @synchronized(self) {
211 bool finished = true; // all dependents must be finished
212 bool cancelled = false; // no dependents can be cancelled
213 bool failed = false; // no dependents can have failed
214 NSMutableArray<NSOperation*>* cancelledSuboperations = [NSMutableArray array];
216 for(CKKSResultOperation* op in operations) {
217 finished &= !!([op isFinished]);
218 cancelled |= !!([op isCancelled]);
219 failed |= (op.error != nil);
221 if([op isCancelled]) {
222 [cancelledSuboperations addObject:op];
225 // TODO: combine suberrors
226 if(op.error != nil) {
227 if([op.error.domain isEqual: CKKSResultErrorDomain] && op.error.code == CKKSResultSubresultError) {
228 // Already a subresult, just copy it on in
229 self.error = op.error;
231 self.error = [NSError errorWithDomain:CKKSResultErrorDomain
232 code:CKKSResultSubresultError
233 description:@"Success-dependent operation failed"
234 underlying:op.error];
239 result = finished && !( cancelled || failed );
241 if(!result && self.error == nil) {
242 self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultCancelled userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Operation (%@) cancelled", cancelledSuboperations]}];
248 + (CKKSResultOperation*)operationWithBlock:(void (^)(void))block {
249 CKKSResultOperation* op = [[self alloc] init];
250 [op addExecutionBlock: block];
254 + (instancetype)named:(NSString*)name withBlock:(void(^)(void)) block {
255 CKKSResultOperation* blockOp = [self operationWithBlock: block];
260 + (instancetype)named:(NSString*)name withBlockTakingSelf:(void(^)(CKKSResultOperation* op))block
262 CKKSResultOperation* op = [[self alloc] init];
263 __weak __typeof(op) weakOp = op;
264 [op addExecutionBlock:^{
265 __strong __typeof(op) strongOp = weakOp;