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+
4957static 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 %.0f s." ,
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
0 commit comments