Skip to content

Commit f06df49

Browse files
authored
Add timeouts for stalled simctl launch and open-url (#19)
1 parent 83eff82 commit f06df49

4 files changed

Lines changed: 112 additions & 6 deletions

File tree

cli/XCWProcessRunner.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ NS_ASSUME_NONNULL_BEGIN
2525
inputData:(nullable NSData *)inputData
2626
error:(NSError * _Nullable * _Nullable)error;
2727

28+
+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
29+
arguments:(NSArray<NSString *> *)arguments
30+
inputData:(nullable NSData *)inputData
31+
timeoutSec:(NSTimeInterval)timeoutSec
32+
error:(NSError * _Nullable * _Nullable)error;
33+
2834
@end
2935

3036
NS_ASSUME_NONNULL_END

cli/XCWProcessRunner.m

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#import <errno.h>
44
#import <fcntl.h>
5+
#import <math.h>
6+
#import <signal.h>
57
#import <spawn.h>
68
#import <string.h>
79
#import <sys/wait.h>
@@ -46,6 +48,12 @@ static void XCWWriteAllAndCloseFD(int fd, NSData *data) {
4648
userInfo:@{ NSLocalizedDescriptionKey: description ?: @"Process failed." }];
4749
}
4850

51+
static NSString *XCWCommandDescription(NSString *launchPath, NSArray<NSString *> *arguments) {
52+
NSMutableArray<NSString *> *parts = [NSMutableArray arrayWithObject:launchPath.lastPathComponent ?: launchPath];
53+
[parts addObjectsFromArray:arguments];
54+
return [parts componentsJoinedByString:@" "];
55+
}
56+
4957
static int XCWCreateTemporaryOutputFile(NSString **path, NSError * _Nullable __autoreleasing *error) {
5058
NSString *templatePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"simdeck-process-XXXXXX"];
5159
char *fileTemplate = strdup(templatePath.fileSystemRepresentation);
@@ -99,6 +107,18 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
99107
arguments:(NSArray<NSString *> *)arguments
100108
inputData:(NSData *)inputData
101109
error:(NSError * _Nullable __autoreleasing *)error {
110+
return [self runLaunchPath:launchPath
111+
arguments:arguments
112+
inputData:inputData
113+
timeoutSec:0
114+
error:error];
115+
}
116+
117+
+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
118+
arguments:(NSArray<NSString *> *)arguments
119+
inputData:(NSData *)inputData
120+
timeoutSec:(NSTimeInterval)timeoutSec
121+
error:(NSError * _Nullable __autoreleasing *)error {
102122
int stdoutFD = -1;
103123
int stderrFD = -1;
104124
int stdinPipe[2] = { -1, -1 };
@@ -208,14 +228,52 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
208228

209229
int waitStatus = 0;
210230
pid_t waitResult = -1;
231+
BOOL timedOut = NO;
232+
BOOL hasTimeout = timeoutSec > 0;
233+
NSDate *deadline = hasTimeout ? [NSDate dateWithTimeIntervalSinceNow:timeoutSec] : nil;
211234
do {
212-
waitResult = waitpid(pid, &waitStatus, 0);
213-
} while (waitResult < 0 && errno == EINTR);
235+
waitResult = waitpid(pid, &waitStatus, hasTimeout ? WNOHANG : 0);
236+
if (waitResult == pid) {
237+
break;
238+
}
239+
if (waitResult < 0 && errno == EINTR) {
240+
continue;
241+
}
242+
if (waitResult < 0) {
243+
break;
244+
}
245+
if (hasTimeout && [deadline timeIntervalSinceNow] <= 0) {
246+
timedOut = YES;
247+
kill(pid, SIGTERM);
248+
NSDate *killDeadline = [NSDate dateWithTimeIntervalSinceNow:2.0];
249+
do {
250+
waitResult = waitpid(pid, &waitStatus, WNOHANG);
251+
if (waitResult == pid || (waitResult < 0 && errno != EINTR)) {
252+
break;
253+
}
254+
usleep(10 * 1000);
255+
} while ([killDeadline timeIntervalSinceNow] > 0);
256+
if (waitResult != pid) {
257+
kill(pid, SIGKILL);
258+
do {
259+
waitResult = waitpid(pid, &waitStatus, 0);
260+
} while (waitResult < 0 && errno == EINTR);
261+
}
262+
break;
263+
}
264+
usleep(10 * 1000);
265+
} while (YES);
214266
if (writeGroup != nil) {
215267
dispatch_group_wait(writeGroup, DISPATCH_TIME_FOREVER);
216268
}
217269
int terminationStatus = 1;
218-
if (waitResult < 0) {
270+
NSString *timeoutMessage = nil;
271+
if (timedOut) {
272+
terminationStatus = 124;
273+
timeoutMessage = [NSString stringWithFormat:@"%@ timed out after %.0fs.",
274+
XCWCommandDescription(launchPath, arguments),
275+
ceil(timeoutSec)];
276+
} else if (waitResult < 0) {
219277
if (error != NULL) {
220278
*error = XCWProcessRunnerError(5, [NSString stringWithFormat:@"Failed to wait for %@: %s", launchPath, strerror(errno)]);
221279
}
@@ -232,6 +290,15 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
232290

233291
NSData *stdoutData = [NSData dataWithContentsOfFile:stdoutPath] ?: [NSData data];
234292
NSData *stderrData = [NSData dataWithContentsOfFile:stderrPath] ?: [NSData data];
293+
if (timeoutMessage.length > 0) {
294+
NSMutableData *combinedStderr = [stderrData mutableCopy];
295+
if (combinedStderr.length > 0) {
296+
const char newline = '\n';
297+
[combinedStderr appendBytes:&newline length:1];
298+
}
299+
[combinedStderr appendData:[timeoutMessage dataUsingEncoding:NSUTF8StringEncoding] ?: [NSData data]];
300+
stderrData = combinedStderr;
301+
}
235302
[[NSFileManager defaultManager] removeItemAtPath:stdoutPath error:nil];
236303
[[NSFileManager defaultManager] removeItemAtPath:stderrPath error:nil];
237304

cli/XCWSimctl.m

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77

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

10+
@interface XCWSimctl ()
11+
12+
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
13+
error:(NSError * _Nullable __autoreleasing *)error;
14+
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
15+
timeoutSec:(NSTimeInterval)timeoutSec
16+
error:(NSError * _Nullable __autoreleasing *)error;
17+
18+
@end
19+
1020
static NSArray *XCWArrayPayload(id payload, NSString *nestedKey) {
1121
if ([payload isKindOfClass:[NSArray class]]) {
1222
return payload;
@@ -252,7 +262,9 @@ - (BOOL)toggleAppearanceForSimulatorUDID:(NSString *)udid error:(NSError * _Null
252262
}
253263

254264
- (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSError * _Nullable __autoreleasing *)error {
255-
XCWProcessResult *result = [self.class runSimctl:@[@"openurl", udid, urlString] error:error];
265+
XCWProcessResult *result = [self.class runSimctl:@[@"openurl", udid, urlString]
266+
timeoutSec:30
267+
error:error];
256268
if (result == nil) {
257269
return NO;
258270
}
@@ -266,7 +278,9 @@ - (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSEr
266278
}
267279

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

445459
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
446460
error:(NSError * _Nullable __autoreleasing *)error {
461+
return [self runSimctl:arguments timeoutSec:0 error:error];
462+
}
463+
464+
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
465+
timeoutSec:(NSTimeInterval)timeoutSec
466+
error:(NSError * _Nullable __autoreleasing *)error {
447467
return [XCWProcessRunner runLaunchPath:@"/usr/bin/xcrun"
448468
arguments:[@[@"simctl"] arrayByAddingObjectsFromArray:arguments]
449469
inputData:nil
470+
timeoutSec:timeoutSec
450471
error:error];
451472
}
452473

server/src/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3160,14 +3160,18 @@ fn service_post_ok(server_url: &str, udid: &str, action: &str, body: &Value) ->
31603160
}
31613161

31623162
fn service_post_error_is_retryable(action: &str, message: &str) -> bool {
3163-
if !matches!(action, "boot" | "shutdown" | "erase") {
3163+
if !matches!(
3164+
action,
3165+
"boot" | "shutdown" | "erase" | "launch" | "open-url"
3166+
) {
31643167
return false;
31653168
}
31663169
let message = message.to_lowercase();
31673170
message.contains("resource temporarily unavailable")
31683171
|| message.contains("connection reset by peer")
31693172
|| message.contains("broken pipe")
31703173
|| message.contains("unexpected eof")
3174+
|| message.contains("timed out")
31713175
}
31723176

31733177
fn http_request_json(
@@ -5219,6 +5223,14 @@ mod tests {
52195223
"boot",
52205224
"Resource temporarily unavailable"
52215225
));
5226+
assert!(service_post_error_is_retryable(
5227+
"launch",
5228+
"SimDeck service returned HTTP 500: xcrun simctl launch timed out after 45s."
5229+
));
5230+
assert!(service_post_error_is_retryable(
5231+
"open-url",
5232+
"Resource temporarily unavailable (os error 35)"
5233+
));
52225234
assert!(!service_post_error_is_retryable(
52235235
"touch",
52245236
"Connection reset by peer (os error 54)"

0 commit comments

Comments
 (0)