Hanging NSTask using waitUntilExit
Asked Answered
S

3

14

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?

Sibel answered 29/10, 2015 at 20:29 Comment(0)
S
29

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:

https://github.com/lroathe/PipeTest

Squeamish answered 1/9, 2016 at 22:3 Comment(2)
Awesome! Thanks for pointing that out - saved me loads of time waiting for a hanging pipe..Quarantine
This saved me. Good catch!Jayme
K
2

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());
Kuomintang answered 31/10, 2015 at 8:2 Comment(0)
G
0

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
Ghana answered 6/4, 2018 at 18:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.