CKKSResultOperation.m   [plain text]

/*
* Copyright (c) 2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/

#if OCTAGON

#import "keychain/ckks/CKKSResultOperation.h"
#import "keychain/ckks/NSOperationCategories.h"
#import "keychain/ckks/CKKSCondition.h"
#import "keychain/categories/NSError+UsefulConstructors.h"
#import "keychain/ot/ObjCImprovements.h"
#import "keychain/ckks/CKKS.h"

@interface CKKSResultOperation()
@property NSMutableArray<CKKSResultOperation*>* successDependencies;
@property bool timeoutCanOccur;
@property dispatch_queue_t timeoutQueue;
@property void (^finishingBlock)(void);
@end

@implementation CKKSResultOperation
- (instancetype)init {
if(self = [super init]) {
WEAKIFY(self);
_error = nil;
_successDependencies = [[NSMutableArray alloc] init];
_timeoutCanOccur = true;
_timeoutQueue = dispatch_queue_create("result-operation-timeout", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
_completionHandlerDidRunCondition = [[CKKSCondition alloc] init];

_finishingBlock = ^(void) {
STRONGIFY(self);
self.finishDate = [NSDate dateWithTimeIntervalSinceNow:0];
};
self.completionBlock = ^{}; // our _finishing block gets added in the method override
}
return self;
}

- (NSString*)operationStateString {
return ([self isFinished] ? [NSString stringWithFormat:@"finished %@", self.finishDate] :
[self isCancelled] ? @"cancelled" :
[self isExecuting] ? @"executing" :
[self isReady] ? @"ready" :
@"pending");
}

- (NSString*)description {
static __thread unsigned __descriptionRecursion = 0;
NSString* state = [self operationStateString];
NSString *desc = NULL;

__descriptionRecursion++;
if(__descriptionRecursion > 10) {
desc = [NSString stringWithFormat: @"<%@: %@ recursion>", [self selfname], state];
} else if(self.error) {
desc = [NSString stringWithFormat: @"<%@: %@ error:%@>", [self selfname], state, self.error];
} else {
desc = [NSString stringWithFormat: @"<%@: %@%@>", [self selfname], state, [self pendingDependenciesString:@" dep:"]];
}
__descriptionRecursion--;
return desc;
}

- (NSString*)debugDescription {
return [self description];
}

- (void)setCompletionBlock:(void (^)(void))completionBlock
{
WEAKIFY(self);
[super setCompletionBlock:^(void) {
STRONGIFY(self);
if (!self) {
ckkserror_global("resultoperation", "completion handler called on deallocated operation instance");
completionBlock(); // go ahead and still behave as things would if this method override were not here
return;
}

self.finishingBlock();
completionBlock();
[self.completionHandlerDidRunCondition fulfill];

for (NSOperation *op in self.dependencies) {
[self removeDependency:op];
}
}];
}

- (void)start {
if(![self allDependentsSuccessful]) {
secdebug("ckksresultoperation", "Not running due to some failed dependent: %@", self.error);
[self cancel];
} else {
[self invalidateTimeout];

}

[super start];
}

- (void)invalidateTimeout {
dispatch_sync(self.timeoutQueue, ^{
if(![self isCancelled]) {
self.timeoutCanOccur = false;
};
});
}

- (NSError* _Nullable)dependenciesDescriptionError {
NSError* underlyingReason = nil;
NSArray* dependencies = [self.dependencies copy];
dependencies = [dependencies objectsAtIndexes: [dependencies indexesOfObjectsPassingTest: ^BOOL (id obj,
NSUInteger idx,
BOOL* stop) {
return [obj isFinished] ? NO : YES;
}]];

for(NSOperation* dependency in dependencies) {
if([dependency isKindOfClass:[CKKSResultOperation class]]) {
CKKSResultOperation* ro = (CKKSResultOperation*)dependency;
underlyingReason = [ro descriptionError] ?: underlyingReason;
}
}

return underlyingReason;
}

// Returns, for this CKKSResultOperation, an error describing this operation or its dependents.
// Used mainly by other CKKSResultOperations who time out waiting for this operation to start/complete.
- (NSError* _Nullable)descriptionError {
static __thread unsigned __descriptionRecursion = 0;

NSError* result = nil;

__descriptionRecursion += 1;

if(self.descriptionErrorCode != 0) {
result = [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
code:self.descriptionErrorCode
userInfo:nil];
} else if(__descriptionRecursion > 10) {
result = [NSError errorWithDomain:CKKSResultDescriptionErrorDomain
code:-1
description:@"Excess recursion"];
} else {
result = [self dependenciesDescriptionError];
}

__descriptionRecursion -= 1;

return result;
}

- (NSError*)_onqueueTimeoutError {
dispatch_assert_queue(self.timeoutQueue);
// Find if any of our dependencies are CKKSResultOperations with a custom reason for existing

NSError* underlyingReason = [self descriptionError];

NSError* error = [NSError errorWithDomain:CKKSResultErrorDomain
code:CKKSResultTimedOut
description:[NSString stringWithFormat:@"Operation(%@) timed out waiting to start for [%@]",
[self selfname],
[self pendingDependenciesString:@""]]
underlying:underlyingReason];
return error;
}

- (instancetype)timeout:(dispatch_time_t)timeout {
WEAKIFY(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.timeoutQueue, ^{
STRONGIFY(self);
if(self.timeoutCanOccur) {
self.error = [self _onqueueTimeoutError];
self.timeoutCanOccur = false;
[self cancel];
}
});

return self;
}

- (void)addSuccessDependency:(CKKSResultOperation *)operation {
[self addNullableSuccessDependency:operation];
}

- (void)addNullableSuccessDependency:(CKKSResultOperation *)operation {
if(!operation) {
return;
}
@synchronized(self) {
[self.successDependencies addObject: operation];
[self addDependency: operation];
}
}

- (bool)allDependentsSuccessful {
return [self allSuccessful: self.successDependencies];
}

- (bool)allSuccessful: (NSArray<CKKSResultOperation*>*) operations {
@synchronized(self) {
bool result = false;

bool finished = true; // all dependents must be finished
bool cancelled = false; // no dependents can be cancelled
bool failed = false; // no dependents can have failed
NSMutableArray<NSOperation*>* cancelledSuboperations = [NSMutableArray array];

for(CKKSResultOperation* op in operations) {
finished &= !!([op isFinished]);
cancelled |= !!([op isCancelled]);
failed |= (op.error != nil);

if([op isCancelled]) {
[cancelledSuboperations addObject:op];
}

// TODO: combine suberrors
if(op.error != nil) {
if([op.error.domain isEqual: CKKSResultErrorDomain] && op.error.code == CKKSResultSubresultError) {
// Already a subresult, just copy it on in
self.error = op.error;
} else {
self.error = [NSError errorWithDomain:CKKSResultErrorDomain
code:CKKSResultSubresultError
description:@"Success-dependent operation failed"
underlying:op.error];
}
}
}

result = finished && !( cancelled || failed );

if(!result && self.error == nil) {
self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultCancelled userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Operation (%@) cancelled", cancelledSuboperations]}];
}
return result;
}
}

+ (CKKSResultOperation*)operationWithBlock:(void (^)(void))block {
CKKSResultOperation* op = [[self alloc] init];
[op addExecutionBlock: block];
return op;
}

+ (instancetype)named:(NSString*)name withBlock:(void(^)(void)) block {
CKKSResultOperation* blockOp = [self operationWithBlock: block];
blockOp.name = name;
return blockOp;
}

+ (instancetype)named:(NSString*)name withBlockTakingSelf:(void(^)(CKKSResultOperation* op))block
{
CKKSResultOperation* op = [[self alloc] init];
__weak __typeof(op) weakOp = op;
[op addExecutionBlock:^{
__strong __typeof(op) strongOp = weakOp;
block(strongOp);
}];
op.name = name;
return op;
}

@end

#endif // OCTAGON