Skip to content

Commit

Permalink
Add client launch retries (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
sewerynplazuk authored Nov 18, 2024
1 parent 5c63045 commit 6be6c4f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 22 deletions.
97 changes: 75 additions & 22 deletions Sources/SBTUITestTunnelClient/SBTUITestTunnelClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ - (void)resetInternalState
self.connected = NO;
self.connectionPort = 0;
self.connectionTimeout = SBTUITunneledApplicationDefaultTimeout;

if (self.ipcConnection) {
[self.ipcConnection invalidate];
self.ipcConnection = nil;
}
if (self.ipcProxy) {
self.ipcProxy = nil;
}
}

- (void)shutDownWithError:(NSError *)error
Expand Down Expand Up @@ -107,11 +115,45 @@ - (void)launchTunnel
}

- (void)launchTunnelWithStartupBlock:(void (^)(void))startupBlock
{
[self launchTunnelWithRetries: 0 retryInterval:0 startupBlock: startupBlock];
}

- (void)launchTunnelWithRetries:(NSInteger)retryThreshold retryInterval:(NSTimeInterval)retryInterval startupBlock:(void (^)(void))startupBlock
{
NSAssert([NSThread isMainThread], @"This method should be invoked from main thread");


__block NSInteger remainingRetries = retryThreshold;

void (^attemptLaunch)(void);
attemptLaunch = ^{
NSError *error = nil;
BOOL success = [self attemptTunnelLaunchWithStartupBlock:startupBlock error:&error];

if (!success) {
remainingRetries--;
[self resetInternalState];

NSLog(@"[SBTUITestTunnel] Launch attempt failed: %@. Remaining retries: %ld", error.localizedDescription, (long)remainingRetries);

if (remainingRetries > 0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
attemptLaunch();
});
} else {
[self shutDownWithErrorMessage:[NSString stringWithFormat:@"[SBTUITestTunnel] Launch failed after all retries: %@", error.localizedDescription] code:SBTUITestTunnelErrorLaunchFailed];
}
return;
}
};

attemptLaunch();
}

- (BOOL)attemptTunnelLaunchWithStartupBlock:(void (^)(void))startupBlock error:(NSError **)error
{
self.launchStart = CFAbsoluteTimeGetCurrent();

NSMutableArray *launchArguments = [self.application.launchArguments mutableCopy];
[launchArguments addObject:SBTUITunneledApplicationLaunchSignal];

Expand All @@ -121,77 +163,88 @@ - (void)launchTunnelWithStartupBlock:(void (^)(void))startupBlock

self.startupBlock = startupBlock;
self.application.launchArguments = launchArguments;

NSMutableDictionary<NSString *, NSString *> *launchEnvironment = [self.application.launchEnvironment mutableCopy];

BOOL useIPC;
#if TARGET_OS_SIMULATOR
NSBundle *bundle = [NSBundle bundleForClass:[SBTUITestTunnelClient class]];
useIPC = !([[bundle objectForInfoDictionaryKey:@"SBTUITestTunnelDisableIPC"] boolValue]);
#else
useIPC = NO;
#endif

if (useIPC) {
NSString *serviceIdentifier = [NSUUID UUID].UUIDString;
self.ipcConnection = [[DTXIPCConnection alloc] initWithServiceName:[NSString stringWithFormat:@"com.subito.sbtuitesttunnel.ipc.%@", serviceIdentifier]];
self.ipcConnection.remoteObjectInterface = [DTXIPCInterface interfaceWithProtocol:@protocol(SBTIPCTunnel)];
self.ipcConnection.exportedInterface = [DTXIPCInterface interfaceWithProtocol:@protocol(SBTIPCTunnel)];
self.ipcConnection.exportedObject = self;

[self.ipcConnection resume];


__block NSError *blockError = nil;
self.ipcProxy = [self.ipcConnection synchronousRemoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
[self shutDownWithErrorMessage:[NSString stringWithFormat:@"[SBTUITestTunnelClient] Failed getting IPC proxy, %@", error.description] code:SBTUITestTunnelErrorLaunchFailed];
blockError = error;
}];

if (error) {
*error = blockError;
}
if (blockError != nil) {
return NO;
}

launchEnvironment[SBTUITunneledApplicationLaunchEnvironmentIPCKey] = serviceIdentifier;
self.application.launchEnvironment = launchEnvironment;
} else {
self.connectionPort = [self findOpenPort];
NSLog(@"[SBTUITestTunnel] Resolving connection on port %ld", self.connectionPort);

if (self.connectionPort < 0) {
return [self shutDownWithErrorMessage:[NSString stringWithFormat:@"[SBTUItestTunnel] Failed finding open port, error: %ld", self.connectionPort] code:SBTUITestTunnelErrorLaunchFailed];
if (error) {
*error = [NSError errorWithDomain:@"SBTUITestTunnel" code:SBTUITestTunnelErrorLaunchFailed userInfo:@{NSLocalizedDescriptionKey: @"Failed finding open port"}];
}
return NO;
}

launchEnvironment[SBTUITunneledApplicationLaunchEnvironmentPortKey] = [NSString stringWithFormat: @"%ld", (long)self.connectionPort];
self.application.launchEnvironment = launchEnvironment;

__weak typeof(self)weakSelf = self;
// Start polling the server with the choosen port
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[weakSelf waitForConnection];
NSLog(@"[SBTUITestTunnel] HTTP tunnel did connect after, %fs", CFAbsoluteTimeGetCurrent() - self.launchStart);

dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.connected = YES;
if (weakSelf.startupBlock) {
weakSelf.startupBlock();
NSLog(@"[SBTUITestTunnel] Did perform startupBlock");
}

NSAssert([NSThread isMainThread], @"We synch on main thread");
weakSelf.startupCompleted = [[self sendSynchronousRequestWithPath:SBTUITunneledApplicationCommandStartupCommandsCompleted params:@{}] isEqualToString:@"YES"];
});
});
}

[self.delegate tunnelClientIsReadyToLaunch:self];

while (YES) {
[NSRunLoop.mainRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];

if (CFAbsoluteTimeGetCurrent() - self.launchStart > SBTUITunneledApplicationDefaultTimeout) {
return [self shutDownWithErrorMessage:[NSString stringWithFormat:@"[SBTUITestTunnel] Waiting for startup block completion timed out"] code:SBTUITestTunnelErrorLaunchFailed];
if (error) {
*error = [NSError errorWithDomain:@"SBTUITestTunnel" code:SBTUITestTunnelErrorLaunchFailed userInfo:@{NSLocalizedDescriptionKey: @"Waiting for startup block completion timed out"}];
}
return NO;
}

if (self.startupCompleted) {
break;
return YES;
}
}

NSLog(@"[SBTUITestTunnel] Tunnel ready after %fs", CFAbsoluteTimeGetCurrent() - self.launchStart);
}

- (void)launchConnectionless:(NSString * (^)(NSString *, NSDictionary<NSString *, NSString *> *))command
Expand Down
15 changes: 15 additions & 0 deletions Sources/SBTUITestTunnelClient/SBTUITunneledApplication.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ - (void)launchTunnelWithOptions:(NSArray<NSString *> *)options startupBlock:(voi
[self launchTunnelWithStartupBlock: startupBlock];
}

- (void)launchTunnelWithOptions:(NSArray<NSString *> *)options retries:(NSInteger)retryThreshold retryInterval:(NSTimeInterval) retryInterval startupBlock:(void (^)(void))startupBlock;
{
NSMutableArray *launchArguments = [self.launchArguments mutableCopy];
[launchArguments addObjectsFromArray:options];

self.launchArguments = launchArguments;

[self launchTunnelWithRetries:retryThreshold retryInterval:retryInterval startupBlock:startupBlock];
}

# pragma mark - SBTUITestTunnelClientDelegate

- (void)tunnelClientIsReadyToLaunch:(SBTUITestTunnelClient *)sender
Expand All @@ -70,6 +80,11 @@ - (void)launchTunnelWithStartupBlock:(void (^)(void))startupBlock
[self.client launchTunnelWithStartupBlock:startupBlock];
}

- (void)launchTunnelWithRetries:(NSInteger)retryThreshold retryInterval:(NSTimeInterval)retryInterval startupBlock:(void (^)(void))startupBlock
{
[self.client launchTunnelWithRetries:retryThreshold retryInterval:retryInterval startupBlock: startupBlock];
}

- (void)launchConnectionless:(NSString * (^)(NSString *, NSDictionary<NSString *,NSString *> *))command
{
[self.client launchConnectionless:command];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@
*/
- (void)launchTunnelWithStartupBlock:(nullable void (^)(void))startupBlock;

/**
* Launch application synchronously waiting for the tunnel server connection to be established.
*
* @param retryThreshold Number of additional launch attempts if initial attempt fails (0 means no retries)
*
* @param retryInterval Time interval in seconds to wait between retry attempts
*
* @param startupBlock Block that is executed before connection is estabilished.
* Useful to inject startup condition (user settings, preferences).
* Note: commands sent in the completionBlock will return nil
*/
- (void)launchTunnelWithRetries:(NSInteger)retryThreshold retryInterval:(NSTimeInterval)retryInterval startupBlock:(nullable void (^)(void))startupBlock;

/**
* Internal, don't use.
*/
Expand Down
18 changes: 18 additions & 0 deletions Sources/SBTUITestTunnelClient/include/SBTUITunneledApplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,22 @@
*/
- (void)launchTunnelWithOptions:(nonnull NSArray<NSString *> *)options startupBlock:(nullable void (^)(void))startupBlock;

/**
* Launch application synchronously waiting for the tunnel server connection to be established.
*
* @param options List of options to be passed on launch.
* Valid options:
* SBTUITunneledApplicationLaunchOptionResetFilesystem: delete app's filesystem sandbox
* SBTUITunneledApplicationLaunchOptionDisableUITextFieldAutocomplete disables UITextField's autocomplete functionality which can lead to unexpected results when typing text.
*
* @param retryThreshold Number of additional launch attempts if initial attempt fails (0 means no retries)
*
* @param retryInterval Time interval in seconds to wait between retry attempts
*
* @param startupBlock Block that is executed before connection is estabilished.
* Useful to inject startup condition (user settings, preferences).
* Note: commands sent in the completionBlock will return nil
*/
- (void)launchTunnelWithOptions:(nonnull NSArray<NSString *> *)options retries:(NSInteger)retryThreshold retryInterval:(NSTimeInterval)retryInterval startupBlock:(nullable void (^)(void))startupBlock;

@end

0 comments on commit 6be6c4f

Please sign in to comment.