I need to use NSTask synchronously, however I find that occasionally my task hangs under the 'waitUntilExit' command. I wonder if there is a graceful way--an error handling method--to terminated the hanging task so I can re-launch another?
Note that if the task being run via NSTask
fills the output pipe then the process will hang, effectively blocking waitUntilExit
from returning.
You can prevent this situation by calling
[task.standardOutput.fileHandleForReading readDataToEndOfFile];
before calling
[task waitUntilExit];
This will cause the output pipe's data to be read until the process writing to the output pipe closes it.
Example code demonstrating the issue and various solutions:
You could launch the task using -[task launch]
and then poll periodically on its isRunning
property to check whether it has already finished. If it has not finished after a given time interval, you can call -[task terminate]
to terminate it. This requires that the task you start does not ignore the SIGTERM signal.
If however polling for task termination is too inefficient in your case, you could setup a dispatch source of type DISPATCH_SOURCE_TYPE_PROC
after you have launched your task. This source then asynchronously calls its event block when the task terminates:
dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, task.processIdentifier, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
Here's an NSTask category which gives you a timeout-friendly waitUntilExit. It also lets you schedule a SIGTERM and SIGKILL.
This method aims to 1) Never spin indefinitely, and 2) Ensure the process gets killed.
@interface NSTask (TRTaskAdditions_termination)
// This method mitigates the infinite-blocking potential of [NSThread waitUntilExit]
//
// Returns a BOOL indicating whether or not the process exited.
//
// Inputs:
// TO: The initial timeout, during which time we wait for the task to exit. (There are additional timeouts if SENTERM or SENDKILL are YES.
// SENDTERM: If we don't exit during the initial timeout, send SIGTERM and wait 2 seconds.
// SENDKILL: If we still haven't exited, send SIGKILL and wait 2 seconds.
//
// The method runs as follows:
// Step 1: Poll [self isRunning] for (TO) seconds. If the task stops running during that time, we return immediately with YES
// Step 2: If the task didn't exit after (TO) seconds, we send it a SIGTERM if (SENDTERM == YES). Wait 2 seconds for the task to exit.
// Step 3: If the task exits, return YES. If it _still_ hasn't exited (i.e. it ignored the SIGTERM,) send it a SIGKILL if (SENDKILL == YES).
// Step 4: Wait another 2 seconds for the task to end. If it exits, return YES. Otherwise, if the task is still running, return NO.
//
// In theory, setting SENDKILL to YES should terminate any process. Processes aren't supposed to be able to escape signal #9 (KILL).
- (BOOL)waitUntilExitWithTimeout:(CFTimeInterval)TO sendTerm:(BOOL)SENDTERM sendKill:(BOOL)SENDKILL;
@end
#include <signal.h>
@implementation NSTask (TRTaskAdditions_termination)
- (BOOL)waitUntilExitWithTimeout:(CFTimeInterval)TO sendTerm:(BOOL)SENDTERM sendKill:(BOOL)SENDKILL
{
CFAbsoluteTime started;
CFAbsoluteTime passed;
BOOL exited = NO;
started = CFAbsoluteTimeGetCurrent();
for (
CFAbsoluteTime now = started;
!exited && ((passed = now - started) < TO);
now = CFAbsoluteTimeGetCurrent()
)
{
if (![self isRunning])
{
exited = YES;
} else {
CFAbsoluteTime sleepTime = 0.1;
useconds_t sleepUsec = round(sleepTime * 1000000.0);
if (sleepUsec == 0) sleepUsec = 1;
usleep(sleepUsec); // sleep for 0.1 sec
}
}
if (!exited)
{
//NSLog(@"%@ didn't exit after timeout of %0.2f sec", self, TO);
if (SENDTERM)
{
TO = 2; // 2 second timeout, waiting for SIGTERM to kill process
//NSLog(@"%@ sending SIGTERM", self);
[self terminate];
/* // UNIX way
pid_t pid = [self processIdentifier];
kill(pid, SIGTERM);
*/
started = CFAbsoluteTimeGetCurrent();
for (
CFAbsoluteTime now = started;
!exited && ((passed = now - started) < TO);
now = CFAbsoluteTimeGetCurrent()
)
{
if (![self isRunning])
{
exited = YES;
} else {
usleep(100000);
}
}
}
if (!exited && SENDKILL)
{
TO = 2; // 2 second timeout, waiting for SIGKILL to kill process
//NSLog(@"%@ sending SIGKILL", self);
pid_t pid = [self processIdentifier];
kill(pid, SIGKILL);
started = CFAbsoluteTimeGetCurrent();
for (
CFAbsoluteTime now = started;
!exited && ((passed = now - started) < TO);
now = CFAbsoluteTimeGetCurrent()
)
{
if (![self isRunning])
{
exited = YES;
} else {
usleep(100000); // sleep for 0.1 sec
}
}
}
}
return exited;
}
@end
© 2022 - 2024 — McMap. All rights reserved.