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;