Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client launch retries #218

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:5 retryInterval:1.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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Why remove this? Maybe needs to be moved higher, next to the attemptLaunchcall?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I will revert this back once we agree on the solution.

}

- (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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not copy/paste here the options, will become hard to maintain. Maybe you can just say something like see method launchTunnelWithOptions for details about the options

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Options are listed in the twin method. I don't want to introduce discrepancies on the interface.

*
* @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
Loading