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;
110 @property bool timeoutCanOccur;
111 @property dispatch_queue_t queue;
114 @implementation OctagonStateTransitionWatcher
116 - (instancetype)initNamed:(NSString*)name
117 serialQueue:(dispatch_queue_t)queue
118 path:(OctagonStateTransitionPath*)pathBeginning
120 if((self = [super init])) {
122 _intendedPath = pathBeginning;
123 _remainingPath = [pathBeginning asPathStep];
125 _result = [CKKSResultOperation named:[NSString stringWithFormat:@"watcher-%@", name] withBlock:^{}];
126 _operationQueue = [[NSOperationQueue alloc] init];
130 _timeoutCanOccur = true;
138 - (NSString*)description {
139 return [NSString stringWithFormat:@"<OctagonStateTransitionWatcher(%@): remaining: %@, result: %@>",
145 - (void)onqueueHandleTransition:(CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)attempt
147 dispatch_assert_queue(self.queue);
149 // Early-exit to make error handling better
150 if(self.remainingPath == nil || self.completed) {
155 [self onqueueProcessTransition:attempt];
158 if([attempt.nextState isEqualToString:self.intendedPath.initialState]) {
160 [self onqueueProcessTransition:attempt];
165 - (instancetype)timeout:(dispatch_time_t)timeout
168 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.queue, ^{
170 if(self.timeoutCanOccur) {
171 self.timeoutCanOccur = false;
173 NSString* description = [NSString stringWithFormat:@"Operation(%@) timed out waiting to start for [%@]",
177 self.result.error = [NSError errorWithDomain:CKKSResultErrorDomain
178 code:CKKSResultTimedOut
179 description:description
181 [self onqueueStartFinishOperation];
189 - (void)onqueueProcessTransition:(CKKSResultOperation<OctagonStateTransitionOperationProtocol>*)attempt
191 dispatch_assert_queue(self.queue);
193 if(self.remainingPath == nil || self.completed) {
198 OctagonStateTransitionPathStep* nextPath = [self.remainingPath nextStep:attempt.nextState];
201 self.remainingPath = nextPath;
202 if(self.remainingPath.successState) {
204 [self onqueueStartFinishOperation];
208 // We're off the path. Error and finish.
210 self.result.error = attempt.error;
212 self.result.error = [NSError errorWithDomain:OctagonErrorDomain
213 code:OTErrorUnexpectedStateTransition
214 description:[NSString stringWithFormat:@"state became %@, was expecting %@", attempt.nextState, self.remainingPath]];
216 [[CKKSAnalytics logger] logUnrecoverableError:self.result.error
217 forEvent:OctagonEventStateTransition
220 @"intended": [self.remainingPath.followStates allKeys],
221 @"became" : attempt.nextState,
224 [self onqueueStartFinishOperation];
228 - (void)onqueueStartFinishOperation {
229 dispatch_assert_queue(self.queue);
231 self.timeoutCanOccur = false;
232 [self.operationQueue addOperation:self.result];
234 self.completed = TRUE;