|  |  |  | // | 
					
						
							|  |  |  | //  Copyright (c) 2019 Open Whisper Systems. All rights reserved. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #import "OWSOperation.h" | 
					
						
							|  |  |  | #import "OWSError.h" | 
					
						
							|  |  |  | #import <SignalCoreKit/OWSAsserts.h> | 
					
						
							|  |  |  | #import <SignalCoreKit/NSObject+OWS.h> | 
					
						
							|  |  |  | #import <SessionUtilitiesKit/OWSBackgroundTask.h> | 
					
						
							|  |  |  | #import <SessionUtilitiesKit/NSTimer+Proxying.h> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NS_ASSUME_NONNULL_BEGIN | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NSString *const OWSOperationKeyIsExecuting = @"isExecuting"; | 
					
						
							|  |  |  | NSString *const OWSOperationKeyIsFinished = @"isFinished"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @interface OWSOperation () | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @property (nullable) NSError *failingError; | 
					
						
							|  |  |  | @property (atomic) OWSOperationState operationState; | 
					
						
							|  |  |  | @property (nonatomic) OWSBackgroundTask *backgroundTask; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // This property should only be accessed on the main queue. | 
					
						
							|  |  |  | @property (nonatomic) NSTimer *_Nullable retryTimer; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @implementation OWSOperation | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (instancetype)init | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     self = [super init]; | 
					
						
							|  |  |  |     if (!self) { | 
					
						
							|  |  |  |         return self; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _operationState = OWSOperationStateNew; | 
					
						
							|  |  |  |     _backgroundTask = [OWSBackgroundTask backgroundTaskWithLabel:self.logTag]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Operations are not retryable by default. | 
					
						
							|  |  |  |     _remainingRetries = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return self; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)dealloc | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSLogDebug(@"in dealloc"); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma mark - Subclass Overrides | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Called one time only | 
					
						
							|  |  |  | - (nullable NSError *)checkForPreconditionError | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     // OWSOperation have a notion of failure, which is inferred by the presence of a `failingError`. | 
					
						
							|  |  |  |     // | 
					
						
							|  |  |  |     // By default, any failing dependency cascades that failure to it's dependent. | 
					
						
							|  |  |  |     // If you'd like different behavior, override this method (`checkForPreconditionError`) without calling `super`. | 
					
						
							|  |  |  |     for (NSOperation *dependency in self.dependencies) { | 
					
						
							|  |  |  |         if (![dependency isKindOfClass:[OWSOperation class]]) { | 
					
						
							|  |  |  |             // Native operations, like NSOperation and NSBlockOperation have no notion of "failure". | 
					
						
							|  |  |  |             // So there's no `failingError` to cascade. | 
					
						
							|  |  |  |             continue; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         OWSOperation *dependentOperation = (OWSOperation *)dependency; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Don't proceed if dependency failed - surface the dependency's error. | 
					
						
							|  |  |  |         NSError *_Nullable dependencyError = dependentOperation.failingError; | 
					
						
							|  |  |  |         if (dependencyError != nil) { | 
					
						
							|  |  |  |             return dependencyError; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return nil; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Called every retry, this is where the bulk of the operation's work should go. | 
					
						
							|  |  |  | - (void)run | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAbstractMethod(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Called at most one time. | 
					
						
							|  |  |  | - (void)didSucceed | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     // no-op | 
					
						
							|  |  |  |     // Override in subclass if necessary | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Called at most one time. | 
					
						
							|  |  |  | - (void)didCancel | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     // no-op | 
					
						
							|  |  |  |     // Override in subclass if necessary | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Called zero or more times, retry may be possible | 
					
						
							|  |  |  | - (void)didReportError:(NSError *)error | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     // no-op | 
					
						
							|  |  |  |     // Override in subclass if necessary | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Called at most one time, once retry is no longer possible. | 
					
						
							|  |  |  | - (void)didFailWithError:(NSError *)error | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     // no-op | 
					
						
							|  |  |  |     // Override in subclass if necessary | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma mark - NSOperation overrides | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Do not override this method in a subclass instead, override `run` | 
					
						
							|  |  |  | - (void)main | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSLogDebug(@"started."); | 
					
						
							|  |  |  |     NSError *_Nullable preconditionError = [self checkForPreconditionError]; | 
					
						
							|  |  |  |     if (preconditionError) { | 
					
						
							|  |  |  |         [self failOperationWithError:preconditionError]; | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (self.isCancelled) { | 
					
						
							|  |  |  |         [self reportCancelled]; | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     [self run]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)runAnyQueuedRetry | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     dispatch_async(dispatch_get_main_queue(), ^{ | 
					
						
							|  |  |  |         NSTimer *_Nullable retryTimer = self.retryTimer; | 
					
						
							|  |  |  |         self.retryTimer = nil; | 
					
						
							|  |  |  |         [retryTimer invalidate]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (retryTimer != nil) { | 
					
						
							|  |  |  |             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ | 
					
						
							|  |  |  |                 [self run]; | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             OWSLogVerbose(@"not re-running since operation is already running."); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma mark - Public Methods | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // These methods are not intended to be subclassed | 
					
						
							|  |  |  | - (void)reportSuccess | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSLogDebug(@"succeeded."); | 
					
						
							|  |  |  |     [self didSucceed]; | 
					
						
							|  |  |  |     [self markAsComplete]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // These methods are not intended to be subclassed | 
					
						
							|  |  |  | - (void)reportCancelled | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSLogDebug(@"cancelled."); | 
					
						
							|  |  |  |     [self didCancel]; | 
					
						
							|  |  |  |     [self markAsComplete]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)reportError:(NSError *)error | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     [self didReportError:error]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (self.remainingRetries == 0) { | 
					
						
							|  |  |  |         [self failOperationWithError:error]; | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self.remainingRetries--; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     dispatch_async(dispatch_get_main_queue(), ^{ | 
					
						
							|  |  |  |         OWSAssertDebug(self.retryTimer == nil); | 
					
						
							|  |  |  |         [self.retryTimer invalidate]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // The `scheduledTimerWith*` methods add the timer to the current thread's RunLoop. | 
					
						
							|  |  |  |         // Since Operations typically run on a background thread, that would mean the background | 
					
						
							|  |  |  |         // thread's RunLoop. However, the OS can spin down background threads if there's no work | 
					
						
							|  |  |  |         // being done, so we run the risk of the timer's RunLoop being deallocated before it's | 
					
						
							|  |  |  |         // fired. | 
					
						
							|  |  |  |         // | 
					
						
							|  |  |  |         // To ensure the timer's thread sticks around, we schedule it while on the main RunLoop. | 
					
						
							|  |  |  |         self.retryTimer = [NSTimer weakScheduledTimerWithTimeInterval:self.retryInterval | 
					
						
							|  |  |  |                                                                target:self | 
					
						
							|  |  |  |                                                              selector:@selector(runAnyQueuedRetry) | 
					
						
							|  |  |  |                                                              userInfo:nil | 
					
						
							|  |  |  |                                                               repeats:NO]; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Override in subclass if you want something more sophisticated, e.g. exponential backoff | 
					
						
							|  |  |  | - (NSTimeInterval)retryInterval | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return 0.1; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma mark - Life Cycle | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)failOperationWithError:(NSError *)error | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSLogDebug(@"failed terminally."); | 
					
						
							|  |  |  |     self.failingError = error; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [self didFailWithError:error]; | 
					
						
							|  |  |  |     [self markAsComplete]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (BOOL)isExecuting | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return self.operationState == OWSOperationStateExecuting; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (BOOL)isFinished | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return self.operationState == OWSOperationStateFinished; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)start | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     [self willChangeValueForKey:OWSOperationKeyIsExecuting]; | 
					
						
							|  |  |  |     self.operationState = OWSOperationStateExecuting; | 
					
						
							|  |  |  |     [self didChangeValueForKey:OWSOperationKeyIsExecuting]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [self main]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (void)markAsComplete | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     [self willChangeValueForKey:OWSOperationKeyIsExecuting]; | 
					
						
							|  |  |  |     [self willChangeValueForKey:OWSOperationKeyIsFinished]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Ensure we call the success or failure handler exactly once. | 
					
						
							|  |  |  |     @synchronized(self) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         OWSAssertDebug(self.operationState != OWSOperationStateFinished); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.operationState = OWSOperationStateFinished; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [self didChangeValueForKey:OWSOperationKeyIsExecuting]; | 
					
						
							|  |  |  |     [self didChangeValueForKey:OWSOperationKeyIsFinished]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NS_ASSUME_NONNULL_END |