3 #import "keychain/categories/NSError+UsefulConstructors.h"
 
   4 #import "keychain/ot/ObjCImprovements.h"
 
   5 #import "keychain/ot/OctagonStateMachineObservers.h"
 
   6 #import "keychain/ot/OTDefines.h"
 
   7 #import "keychain/ot/OTConstants.h"
 
   9 @implementation OctagonStateTransitionPathStep
 
  11 - (instancetype)initAsSuccess
 
  13     if((self = [super init])) {
 
  19 - (instancetype)initWithPath:(NSDictionary<OctagonState*, OctagonStateTransitionPathStep*>*)followStates
 
  21     if((self = [super init])) {
 
  23         _followStates = followStates;
 
  28 - (OctagonStateTransitionPathStep*)nextStep:(OctagonState*)stateStep
 
  30     // If stateStep matches a followState, return it. Otherwise, return nil.
 
  31     return self.followStates[stateStep];
 
  34 - (NSString*)description
 
  36     return [NSString stringWithFormat:@"<OSTPath(%@)>", self.followStates.allKeys];
 
  39 + (OctagonStateTransitionPathStep*)success
 
  41     return [[OctagonStateTransitionPathStep alloc] initAsSuccess];
 
  44 + (OctagonStateTransitionPathStep*)pathFromDictionary:(NSDictionary<OctagonState*, id>*)pathDict
 
  46     NSMutableDictionary<OctagonState*, OctagonStateTransitionPathStep*>* converted = [NSMutableDictionary dictionary];
 
  47     for(id key in pathDict.allKeys) {
 
  48         id obj = pathDict[key];
 
  50         if([obj isKindOfClass:[OctagonStateTransitionPathStep class]]) {
 
  52         } else if([obj isKindOfClass:[NSDictionary class]]) {
 
  53             converted[key] = [OctagonStateTransitionPathStep pathFromDictionary:(NSDictionary*)obj];
 
  57     if([converted count] == 0) {
 
  58         return [[OctagonStateTransitionPathStep alloc] initAsSuccess];
 
  61     return [[OctagonStateTransitionPathStep alloc] initWithPath:converted];
 
  65 #pragma mark - OctagonStateTransitionPath
 
  67 @implementation OctagonStateTransitionPath
 
  68 - (instancetype)initWithState:(OctagonState*)initialState
 
  69                      pathStep:(OctagonStateTransitionPathStep*)pathStep
 
  71     if((self = [super init])) {
 
  72         _initialState = initialState;
 
  78 - (OctagonStateTransitionPathStep*)asPathStep
 
  80     return [[OctagonStateTransitionPathStep alloc] initWithPath:@{
 
  81         self.initialState: self.pathStep,
 
  85 + (OctagonStateTransitionPath* _Nullable)pathFromDictionary:(NSDictionary<OctagonState*, id>*)pathDict
 
  87     for(id key in pathDict.allKeys) {
 
  88         id obj = pathDict[key];
 
  90         if([obj isKindOfClass:[OctagonStateTransitionPathStep class]]) {
 
  91             return [[OctagonStateTransitionPath alloc] initWithState:key
 
  93         } else if([obj isKindOfClass:[NSDictionary class]]) {
 
  94             return [[OctagonStateTransitionPath alloc] initWithState:key
 
  95                                                             pathStep:[OctagonStateTransitionPathStep pathFromDictionary:obj]];
 
 103 #pragma mark - OctagonStateTransitionWatcher
 
 105 @interface OctagonStateTransitionWatcher ()
 
 106 @property BOOL active;
 
 107 @property BOOL completed;
 
 108 @property (nullable) OctagonStateTransitionPathStep* remainingPath;
 
 109 @property NSOperationQueue* operationQueue;
 
 111 @property (nullable) OctagonStateTransitionRequest* initialRequest;
 
 112 @property (nullable) CKKSResultOperation* initialTimeoutListenerOp;
 
 114 @property bool timeoutCanOccur;
 
 115 @property dispatch_queue_t queue;
 
 118 @implementation OctagonStateTransitionWatcher
 
 120 - (instancetype)initNamed:(NSString*)name
 
 121               serialQueue:(dispatch_queue_t)queue
 
 122                      path:(OctagonStateTransitionPath*)pathBeginning
 
 123            initialRequest:(OctagonStateTransitionRequest* _Nullable)initialRequest
 
 125     if((self = [super init])) {
 
 127         _intendedPath = pathBeginning;
 
 128         _remainingPath = [pathBeginning asPathStep];
 
 130         _result = [CKKSResultOperation named:[NSString stringWithFormat:@"watcher-%@", name] withBlock:^{}];
 
 131         _operationQueue = [[NSOperationQueue alloc] init];
 
 135         _timeoutCanOccur = true;
 
 136         _initialRequest = initialRequest;
 
 139             _initialTimeoutListenerOp = [CKKSResultOperation named:[NSString stringWithFormat:@"watcher-timeout-%@", name] withBlock:^{
 
 145                 NSError* opError = initialRequest.transitionOperation.error;
 
 148                    [opError.domain isEqualToString:CKKSResultErrorDomain] &&
 
 149                    opError.code == CKKSResultTimedOut) {
 
 150                     dispatch_sync(self.queue, ^{
 
 151                         [self _onqueuePerformTimeoutWithUnderlyingError:opError];
 
 155             [_initialTimeoutListenerOp addDependency:initialRequest.transitionOperation];
 
 156             [_operationQueue addOperation:_initialTimeoutListenerOp];
 
 165 - (NSString*)description {
 
 166     return [NSString stringWithFormat:@"<OctagonStateTransitionWatcher(%@): remaining: %@, result: %@>",
 
 172 - (void)onqueueHandleTransition:(CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)attempt
 
 174     dispatch_assert_queue(self.queue);
 
 176     // Early-exit to make error handling better
 
 177     if(self.remainingPath == nil || self.completed) {
 
 182         [self onqueueProcessTransition:attempt];
 
 185         if([attempt.nextState isEqualToString:self.intendedPath.initialState]) {
 
 187             [self onqueueProcessTransition:attempt];
 
 192 - (void)_onqueuePerformTimeoutWithUnderlyingError:(NSError* _Nullable)underlyingError
 
 194     dispatch_assert_queue(self.queue);
 
 196     if(self.timeoutCanOccur) {
 
 197         self.timeoutCanOccur = false;
 
 199         NSString* description = [NSString stringWithFormat:@"Operation(%@) timed out waiting to start for [%@]",
 
 203         self.result.error = [NSError errorWithDomain:CKKSResultErrorDomain
 
 204                                                 code:CKKSResultTimedOut
 
 205                                          description:description
 
 206                                           underlying:underlyingError];
 
 207         [self onqueueStartFinishOperation];
 
 211 - (instancetype)timeout:(dispatch_time_t)timeout
 
 214     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.queue, ^{
 
 216         [self _onqueuePerformTimeoutWithUnderlyingError:nil];
 
 222 - (void)onqueueProcessTransition:(CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)attempt
 
 224     dispatch_assert_queue(self.queue);
 
 226     if(self.remainingPath == nil || self.completed) {
 
 230     OctagonStateTransitionPathStep* nextPath = [self.remainingPath nextStep:attempt.nextState];
 
 233         self.remainingPath = nextPath;
 
 234         if(self.remainingPath.successState) {
 
 236             [self onqueueStartFinishOperation];
 
 240         // We're off the path. Error and finish.
 
 242             self.result.error = attempt.error;
 
 244             self.result.error = [NSError errorWithDomain:OctagonErrorDomain
 
 245                                                     code:OTErrorUnexpectedStateTransition
 
 246                                              description:[NSString stringWithFormat:@"state became %@, was expecting %@", attempt.nextState, self.remainingPath]];
 
 248         [[CKKSAnalytics logger] logUnrecoverableError:self.result.error
 
 249                                              forEvent:OctagonEventStateTransition
 
 252                                                         @"intended": [self.remainingPath.followStates allKeys],
 
 253                                                         @"became" : attempt.nextState,
 
 256         [self onqueueStartFinishOperation];
 
 260 - (void)onqueueStartFinishOperation {
 
 261     dispatch_assert_queue(self.queue);
 
 263     self.timeoutCanOccur = false;
 
 264     [self.operationQueue addOperation:self.result];
 
 266     self.completed = TRUE;
 
 271 #pragma mark - OctagonStateMultiStateArrivalWatcher
 
 273 @interface OctagonStateMultiStateArrivalWatcher ()
 
 274 @property BOOL completed;
 
 275 @property NSOperationQueue* operationQueue;
 
 277 @property (nullable) CKKSResultOperation* initialTimeoutListenerOp;
 
 279 @property bool timeoutCanOccur;
 
 280 @property dispatch_queue_t queue;
 
 284 @implementation OctagonStateMultiStateArrivalWatcher
 
 285 - (instancetype)initNamed:(NSString*)name
 
 286               serialQueue:(dispatch_queue_t)queue
 
 287                    states:(NSSet<OctagonState*>*)states
 
 289     if((self = [super init])) {
 
 293         _result = [CKKSResultOperation named:[NSString stringWithFormat:@"watcher-%@", name] withBlock:^{}];
 
 294         _operationQueue = [[NSOperationQueue alloc] init];
 
 297         _timeoutCanOccur = true;
 
 304 - (void)onqueueHandleTransition:(CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)attempt
 
 306     dispatch_assert_queue(self.queue);
 
 307     [self onqueueEnterState:attempt.nextState];
 
 310 - (void)onqueueEnterState:(OctagonState*)state
 
 312     if(!self.completed) {
 
 313         if([self.states containsObject:state]) {
 
 314             [self onqueueStartFinishOperation];
 
 319 - (void)_onqueuePerformTimeoutWithUnderlyingError
 
 321     dispatch_assert_queue(self.queue);
 
 323     if(self.timeoutCanOccur) {
 
 324         self.timeoutCanOccur = false;
 
 326         NSString* description = [NSString stringWithFormat:@"Operation(%@) timed out waiting to start for any state in [%@]",
 
 330         self.result.error = [NSError errorWithDomain:CKKSResultErrorDomain
 
 331                                                 code:CKKSResultTimedOut
 
 332                                          description:description];
 
 333         [self onqueueStartFinishOperation];
 
 337 - (instancetype)timeout:(dispatch_time_t)timeout
 
 340     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.queue, ^{
 
 342         [self _onqueuePerformTimeoutWithUnderlyingError];
 
 348 - (void)onqueueStartFinishOperation {
 
 349     dispatch_assert_queue(self.queue);
 
 351     self.timeoutCanOccur = false;
 
 352     [self.operationQueue addOperation:self.result];
 
 353     self.completed = TRUE;