node pty.js spawning a process which itself spawns child process and the child doesn't die when node is killed

StackOverflow https://stackoverflow.com/questions/22514354

Question

Using Ubuntu 13.10 and running node v0.10.0. I'm using pty.js v0.2.4 to spawn a program (which needs to run in an interactive environment). The program is written in C and forks a child process itself.

I've written a very cut down version of the C program (which I've called 'forktest') which has the minimum required to produce this problem and contains the following:

#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(void)
{
    pid_t childPID;
    childPID = fork();
    if(childPID >= 0) {
        if(childPID == 0) {
            signal(SIGHUP, SIG_IGN);
            while(1) {
              printf("Child Process\n");
              fflush(stdout);
              usleep(500000);
            }
        } else {
            printf("Parent process\n");
            fflush(stdout);
            getchar();
        }
    } else {
        printf("Fork failed, exiting\n");
        return 1;
    }
    return 0;
}

I've also put together a minimal node script, written in coffeescript (test.coffee), which runs the program and looks like this:

pty = require 'pty.js'

command = './forktest'
example = pty.spawn command, null
example.on 'data', (data) ->
  console.log data
example.on 'exit', ->
  console.log 'example exited'

When running the node script 'coffee test.coffee', the output ('Parent Process' and 'Child Process') is seen. Also the hierarchy of the processes appear correctly in ps ('ps faux'):

lightdm
 \_ /usr/bin/X -core :0 -auth /var/run/lightdm/root/:0 -no
 \_ lightdm --session-child 12 21
     \_ init --user
         \_ gnome-terminal
             \_ bash
                 \_ node /usr/local/bin/coffee test.coffee
                     \_ ./forktest
                         \_ ./forktest

But when quitting coffeescript (with ctrl-c) the parent forktest process is also quit as expected but the child forktest process gets left behind, and the output of 'ps faux' then looks like this:

lightdm
 \_ /usr/bin/X -core :
 \_ lightdm --session-
     \_ init --user
         \_ ./forktest

I've investigated that this is because the hang-up signal is being ignored in the child process:

signal(SIGHUP, SIG_IGN);

If I use spawn from the child_process module in node:

{spawn} = require 'child_process'

command = './forktest'
example = spawn command

example.stdout.on 'data', (data) ->
  console.log String data
example.on 'exit', ->
  console.log 'example exited'

And quit the coffeescript app in the same way (ctrl-c), when it quits both the parent and child processes of forktest disappear so it looks to be related to how pty.js works in some way.

I'm not in a position to edit (remove the ignoring of the signal) and distribute the original C program to users so I'm looking for a way around this.

One workaround is to catch the interrupt signal of node with:

process.on 'SIGINT', (data) ->
  # kill the parent / child process manually

Which I should probably do anyway..but if the coffeescript process is killed, rather than interrupted, it will still suffer from the same issue and leave the child behind. As far as I know there isn't a way to catch a SIGKILL in node?

Is there a reason that child processes of of program spawned with pty.js aren't destroyed when node is killed?

Was it helpful?

Solution

You are running into a subtle issue with how POSIX systems handle killing processes.

When the process is killed, then the kernel redefines who the parent is. Unlike when a child exits and notifies its parent, if the parent process does not send a signal down to the child on exit, then the child receives no notification. The kernel simply redefines its parent id to the init process.

Therefore, by default no child exits when it's parent exits. So how do children get killed at all when a parent quits? Well, every process has a process group id. This defaults to the process group id of it's parent on start. If a process group becomes orphaned by the death of a process, then the kernel might send a SIGHUP and SIGCONT to all members of the process group. This gives the children a chance to exit, but they don't have to. If they do not quit, then they are orphaned processes, and they get their parent id set to the init process.

Coincidentally, this is how forking to create a daemon process works.

So, to resolve your problem, make sure that the grandchildren are part of the same process group id. This should let them receive the signal that kills the child process. However, if they ignore the signal, you really only have one option.

Spawn a wrapper program that does nothing but spawn your actual process and passes everything back and forth between the "real" parent and the "real" child. Then, if the SIGHUP gets sent to your wrapper, send a SIGKILL to the child to force an exit.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top