Ensure a subprocess is dead in Cocoa
Asked Answered
Y

5

9

I'm writing an application that kicks off a subprocess running a simple web server. I am using NSTask and communicating with it with pipes, and everything seems more or less fine. However, if my program crashes, the subprocess is left alive and the next time I launch the app there is a conflict between the old subprocess and the new one. Is there any way to ensure that subprocesses die when the owning app dies?

Yardstick answered 18/8, 2009 at 19:33 Comment(3)
Have you tried making your program not crash? :DScabies
I'm thinking about Atlas here ^^Logger
You could try running the web server in Launchd. At least that way, when your app crashes and you relaunch, Launchd will tell you that the server is already running so you can shut it down and restart it if you want.Outbreak
F
3

None of the above works… Not even launchd in all it's crappily-documented-complexity has a way to handle this common scenario. I dunno why Apple doesn't just make a "mothership-approved" way to run background processes, but whatever.. my solution is…

  1. Launch a shell script via NSTask and pass it any variables you need. Also pass in your parent process' PID via int masterPID = [[NSProcessInfo processInfo] processIdentifier]; etc. Read these in your script via $1, $2, etc.

  2. In turn, launch your subprocesses from inside the script..

  3. Monitor both the subprocess AND your parent process within the script.

This serves a dual purpose.. it enables you to "keep an eye on the kids..", and in the sad event of parentcide (or horrible car accident) - kill-off the zombie orphans. Then, you pull the trigger on yourself (you being the shell script) and your process table will be clean.. as if you never existed. No blocked ports, no conflicts on relaunch, no app-store rejections. Lemme know if this helps!

Update: I made a Xcode template / daemon / project / whatever that does the trick. Check it out.. mralexgray / Infanticide.

Fumarole answered 7/1, 2012 at 21:7 Comment(0)
H
1

Your application delegate can implement the

- (void)applicationWillTerminate:(NSNotification *)aNotification

message, and terminate the NSTask there. However, it is not guaranteed that during a crash, this delegate will be called.

Two additional steps you can take:

  • Shutdown an existing, orphaned subprocess during the launch of a new parent-process by writing down to disk the PID of the subprocess on creation and removing it during normal shutdown (sometimes not the safest behavior).
  • Shutdown the subprocess if the NSPipe's end-point didn't send data for a specific amount of time (something like a heartbeat).
Hand answered 18/8, 2009 at 19:42 Comment(0)
O
1

Following code sample should help you.

it is borrowed from here,

#include <CoreFoundation/CoreFoundation.h>
#include <unistd.h>
#include <sys/event.h>
static void noteProcDeath(CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info) {
    struct kevent kev;
    int fd = CFFileDescriptorGetNativeDescriptor(fdref);
    kevent(fd, NULL, 0, &kev, 1, NULL);
    // take action on death of process here
    printf("process with pid '%u' died\n", (unsigned int)kev.ident);
    CFFileDescriptorInvalidate(fdref);
    CFRelease(fdref); // the CFFileDescriptorRef is no longer of any use in this example
}
// one argument, an integer pid to watch, required
int main(int argc, char *argv[]) {
    if (argc < 2) exit(1);
    int fd = kqueue();
    struct kevent kev;
    EV_SET(&kev, atoi(argv[1]), EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL);
    kevent(fd, &kev, 1, NULL, 0, NULL);
    CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, fd, true, noteProcDeath, NULL);
    CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
    CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
    CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
    // run the run loop for 20 seconds
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 20.0, false);
    return 0;
}
Oared answered 16/4, 2013 at 11:28 Comment(0)
B
0

UPDATE: Now that I go to check this properly it doesn't work. Trying to set the process group fails with this error;

EPERM "The effective user ID of the requested process is different from that of the caller and the process is not a descendant of the calling process."

There is a more recent thread on this issue but no easy solution as far as I can tell

http://www.omnigroup.com/mailman/archive/macosx-dev/2009-March/062164.html


I've tried a suggestion by Robert Pointon on Cocoadev in my app. I haven't got around to testing it yet though.

http://www.cocoadev.com/index.pl?NSTaskTermination

The idea is to set the process group of the task to be the same as that of the process that launches the task (note: the code below is basically lifted from the thread above).

    pid_t group = setsid();
    if (group == -1) {
        group = getpgrp();
    }

   [task launch]; 


if (setpgid([task processIdentifier], group) == -1) {
   NSLog(@"unable to put task into same group as self");
   [task terminate];
} else {
// handle running task
}
Bustamante answered 19/8, 2009 at 6:24 Comment(0)
A
0

There's -[NSConcreteTask setStartsNewProcessGroup:] private API, if you pass NO, then created NSTask instance will not detach child process from the process group of the current process, and it will die as soon as parent process dies.

Kudos to xctool: https://github.com/facebook/xctool/pull/159/files

Anastaciaanastas answered 17/8, 2018 at 12:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.