Skip to content
Merged
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
6 changes: 6 additions & 0 deletions cli/XCWProcessRunner.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ NS_ASSUME_NONNULL_BEGIN
inputData:(nullable NSData *)inputData
error:(NSError * _Nullable * _Nullable)error;

+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
arguments:(NSArray<NSString *> *)arguments
inputData:(nullable NSData *)inputData
timeoutSec:(NSTimeInterval)timeoutSec
error:(NSError * _Nullable * _Nullable)error;

@end

NS_ASSUME_NONNULL_END
73 changes: 70 additions & 3 deletions cli/XCWProcessRunner.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#import <errno.h>
#import <fcntl.h>
#import <math.h>
#import <signal.h>
#import <spawn.h>
#import <string.h>
#import <sys/wait.h>
Expand Down Expand Up @@ -46,6 +48,12 @@ static void XCWWriteAllAndCloseFD(int fd, NSData *data) {
userInfo:@{ NSLocalizedDescriptionKey: description ?: @"Process failed." }];
}

static NSString *XCWCommandDescription(NSString *launchPath, NSArray<NSString *> *arguments) {
NSMutableArray<NSString *> *parts = [NSMutableArray arrayWithObject:launchPath.lastPathComponent ?: launchPath];
[parts addObjectsFromArray:arguments];
return [parts componentsJoinedByString:@" "];
}

static int XCWCreateTemporaryOutputFile(NSString **path, NSError * _Nullable __autoreleasing *error) {
NSString *templatePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"simdeck-process-XXXXXX"];
char *fileTemplate = strdup(templatePath.fileSystemRepresentation);
Expand Down Expand Up @@ -99,6 +107,18 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
arguments:(NSArray<NSString *> *)arguments
inputData:(NSData *)inputData
error:(NSError * _Nullable __autoreleasing *)error {
return [self runLaunchPath:launchPath
arguments:arguments
inputData:inputData
timeoutSec:0
error:error];
}

+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
arguments:(NSArray<NSString *> *)arguments
inputData:(NSData *)inputData
timeoutSec:(NSTimeInterval)timeoutSec
error:(NSError * _Nullable __autoreleasing *)error {
int stdoutFD = -1;
int stderrFD = -1;
int stdinPipe[2] = { -1, -1 };
Expand Down Expand Up @@ -208,14 +228,52 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath

int waitStatus = 0;
pid_t waitResult = -1;
BOOL timedOut = NO;
BOOL hasTimeout = timeoutSec > 0;
NSDate *deadline = hasTimeout ? [NSDate dateWithTimeIntervalSinceNow:timeoutSec] : nil;
do {
waitResult = waitpid(pid, &waitStatus, 0);
} while (waitResult < 0 && errno == EINTR);
waitResult = waitpid(pid, &waitStatus, hasTimeout ? WNOHANG : 0);
if (waitResult == pid) {
break;
}
if (waitResult < 0 && errno == EINTR) {
continue;
}
if (waitResult < 0) {
break;
}
if (hasTimeout && [deadline timeIntervalSinceNow] <= 0) {
timedOut = YES;
kill(pid, SIGTERM);
NSDate *killDeadline = [NSDate dateWithTimeIntervalSinceNow:2.0];
do {
waitResult = waitpid(pid, &waitStatus, WNOHANG);
if (waitResult == pid || (waitResult < 0 && errno != EINTR)) {
break;
}
usleep(10 * 1000);
} while ([killDeadline timeIntervalSinceNow] > 0);
if (waitResult != pid) {
kill(pid, SIGKILL);
do {
waitResult = waitpid(pid, &waitStatus, 0);
} while (waitResult < 0 && errno == EINTR);
}
break;
}
usleep(10 * 1000);
} while (YES);
if (writeGroup != nil) {
dispatch_group_wait(writeGroup, DISPATCH_TIME_FOREVER);
}
int terminationStatus = 1;
if (waitResult < 0) {
NSString *timeoutMessage = nil;
if (timedOut) {
terminationStatus = 124;
timeoutMessage = [NSString stringWithFormat:@"%@ timed out after %.0fs.",
XCWCommandDescription(launchPath, arguments),
ceil(timeoutSec)];
} else if (waitResult < 0) {
if (error != NULL) {
*error = XCWProcessRunnerError(5, [NSString stringWithFormat:@"Failed to wait for %@: %s", launchPath, strerror(errno)]);
}
Expand All @@ -232,6 +290,15 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath

NSData *stdoutData = [NSData dataWithContentsOfFile:stdoutPath] ?: [NSData data];
NSData *stderrData = [NSData dataWithContentsOfFile:stderrPath] ?: [NSData data];
if (timeoutMessage.length > 0) {
NSMutableData *combinedStderr = [stderrData mutableCopy];
if (combinedStderr.length > 0) {
const char newline = '\n';
[combinedStderr appendBytes:&newline length:1];
}
[combinedStderr appendData:[timeoutMessage dataUsingEncoding:NSUTF8StringEncoding] ?: [NSData data]];
stderrData = combinedStderr;
}
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];

Expand Down
25 changes: 23 additions & 2 deletions cli/XCWSimctl.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

static NSString * const XCWSimctlErrorDomain = @"SimDeck.Simctl";

@interface XCWSimctl ()

+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
error:(NSError * _Nullable __autoreleasing *)error;
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
timeoutSec:(NSTimeInterval)timeoutSec
error:(NSError * _Nullable __autoreleasing *)error;

@end

static NSArray *XCWArrayPayload(id payload, NSString *nestedKey) {
if ([payload isKindOfClass:[NSArray class]]) {
return payload;
Expand Down Expand Up @@ -252,7 +262,9 @@ - (BOOL)toggleAppearanceForSimulatorUDID:(NSString *)udid error:(NSError * _Null
}

- (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSError * _Nullable __autoreleasing *)error {
XCWProcessResult *result = [self.class runSimctl:@[@"openurl", udid, urlString] error:error];
XCWProcessResult *result = [self.class runSimctl:@[@"openurl", udid, urlString]
timeoutSec:30
error:error];
if (result == nil) {
return NO;
}
Expand All @@ -266,7 +278,9 @@ - (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSEr
}

- (BOOL)launchBundleID:(NSString *)bundleID simulatorUDID:(NSString *)udid error:(NSError * _Nullable __autoreleasing *)error {
XCWProcessResult *result = [self.class runSimctl:@[@"launch", @"--stdout=/dev/null", @"--stderr=/dev/null", udid, bundleID] error:error];
XCWProcessResult *result = [self.class runSimctl:@[@"launch", @"--stdout=/dev/null", @"--stderr=/dev/null", udid, bundleID]
timeoutSec:45
error:error];
if (result == nil) {
return NO;
}
Expand Down Expand Up @@ -444,9 +458,16 @@ - (nullable NSString *)pasteboardTextForSimulatorUDID:(NSString *)udid error:(NS

+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
error:(NSError * _Nullable __autoreleasing *)error {
return [self runSimctl:arguments timeoutSec:0 error:error];
}

+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
timeoutSec:(NSTimeInterval)timeoutSec
error:(NSError * _Nullable __autoreleasing *)error {
return [XCWProcessRunner runLaunchPath:@"/usr/bin/xcrun"
arguments:[@[@"simctl"] arrayByAddingObjectsFromArray:arguments]
inputData:nil
timeoutSec:timeoutSec
error:error];
}

Expand Down
14 changes: 13 additions & 1 deletion server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3160,14 +3160,18 @@ fn service_post_ok(server_url: &str, udid: &str, action: &str, body: &Value) ->
}

fn service_post_error_is_retryable(action: &str, message: &str) -> bool {
if !matches!(action, "boot" | "shutdown" | "erase") {
if !matches!(
action,
"boot" | "shutdown" | "erase" | "launch" | "open-url"
) {
return false;
}
let message = message.to_lowercase();
message.contains("resource temporarily unavailable")
|| message.contains("connection reset by peer")
|| message.contains("broken pipe")
|| message.contains("unexpected eof")
|| message.contains("timed out")
}

fn http_request_json(
Expand Down Expand Up @@ -5048,6 +5052,14 @@ mod tests {
"boot",
"Resource temporarily unavailable"
));
assert!(service_post_error_is_retryable(
"launch",
"SimDeck service returned HTTP 500: xcrun simctl launch timed out after 45s."
));
assert!(service_post_error_is_retryable(
"open-url",
"Resource temporarily unavailable (os error 35)"
));
assert!(!service_post_error_is_retryable(
"touch",
"Connection reset by peer (os error 54)"
Expand Down
Loading