From 90407cb7c1702e08479f667b849ed35e8ac70c01 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 5 May 2026 22:43:50 -0400 Subject: [PATCH] Add timeouts for stalled simctl launch and open-url --- cli/XCWProcessRunner.h | 6 ++++ cli/XCWProcessRunner.m | 73 ++++++++++++++++++++++++++++++++++++++++-- cli/XCWSimctl.m | 25 +++++++++++++-- server/src/main.rs | 14 +++++++- 4 files changed, 112 insertions(+), 6 deletions(-) diff --git a/cli/XCWProcessRunner.h b/cli/XCWProcessRunner.h index e4c94156..70626ab2 100644 --- a/cli/XCWProcessRunner.h +++ b/cli/XCWProcessRunner.h @@ -25,6 +25,12 @@ NS_ASSUME_NONNULL_BEGIN inputData:(nullable NSData *)inputData error:(NSError * _Nullable * _Nullable)error; ++ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath + arguments:(NSArray *)arguments + inputData:(nullable NSData *)inputData + timeoutSec:(NSTimeInterval)timeoutSec + error:(NSError * _Nullable * _Nullable)error; + @end NS_ASSUME_NONNULL_END diff --git a/cli/XCWProcessRunner.m b/cli/XCWProcessRunner.m index d6953ae2..fbf61028 100644 --- a/cli/XCWProcessRunner.m +++ b/cli/XCWProcessRunner.m @@ -2,6 +2,8 @@ #import #import +#import +#import #import #import #import @@ -46,6 +48,12 @@ static void XCWWriteAllAndCloseFD(int fd, NSData *data) { userInfo:@{ NSLocalizedDescriptionKey: description ?: @"Process failed." }]; } +static NSString *XCWCommandDescription(NSString *launchPath, NSArray *arguments) { + NSMutableArray *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); @@ -99,6 +107,18 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath arguments:(NSArray *)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 *)arguments + inputData:(NSData *)inputData + timeoutSec:(NSTimeInterval)timeoutSec + error:(NSError * _Nullable __autoreleasing *)error { int stdoutFD = -1; int stderrFD = -1; int stdinPipe[2] = { -1, -1 }; @@ -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)]); } @@ -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]; diff --git a/cli/XCWSimctl.m b/cli/XCWSimctl.m index 78434905..f2840b50 100644 --- a/cli/XCWSimctl.m +++ b/cli/XCWSimctl.m @@ -7,6 +7,16 @@ static NSString * const XCWSimctlErrorDomain = @"SimDeck.Simctl"; +@interface XCWSimctl () + ++ (nullable XCWProcessResult *)runSimctl:(NSArray *)arguments + error:(NSError * _Nullable __autoreleasing *)error; ++ (nullable XCWProcessResult *)runSimctl:(NSArray *)arguments + timeoutSec:(NSTimeInterval)timeoutSec + error:(NSError * _Nullable __autoreleasing *)error; + +@end + static NSArray *XCWArrayPayload(id payload, NSString *nestedKey) { if ([payload isKindOfClass:[NSArray class]]) { return payload; @@ -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; } @@ -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; } @@ -444,9 +458,16 @@ - (nullable NSString *)pasteboardTextForSimulatorUDID:(NSString *)udid error:(NS + (nullable XCWProcessResult *)runSimctl:(NSArray *)arguments error:(NSError * _Nullable __autoreleasing *)error { + return [self runSimctl:arguments timeoutSec:0 error:error]; +} + ++ (nullable XCWProcessResult *)runSimctl:(NSArray *)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]; } diff --git a/server/src/main.rs b/server/src/main.rs index 7992a0c5..9fb73d1d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3160,7 +3160,10 @@ 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(); @@ -3168,6 +3171,7 @@ fn service_post_error_is_retryable(action: &str, message: &str) -> bool { || message.contains("connection reset by peer") || message.contains("broken pipe") || message.contains("unexpected eof") + || message.contains("timed out") } fn http_request_json( @@ -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)"