Skip to content

Instantly share code, notes, and snippets.

@coltrane
Created May 5, 2012 04:51
Show Gist options
  • Save coltrane/2599899 to your computer and use it in GitHub Desktop.
Save coltrane/2599899 to your computer and use it in GitHub Desktop.
Tests the response of node.js child processes to various posix signals.
var child_process = require('child_process'),
assert = require('assert')
var isChild = !!(process.send),
isMaster = ((!isChild) && (process.argv.length > 2)),
isTopLevel = (!isMaster && !isChild)
if( isTopLevel ) {
//**
//* TopLevel - The program was called with no arguments. We'll run the test
//* once for each stop-signal ('T' or 'A') in the list. (except, we'll skip
//* SIGUSR1, because node's debugger owns that one.
//*
(function top_level(){
var all_signals = [
// A list of all posix-defined signals. "The following signals shall be supported on
// all implementations".
// See: <http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html>
//
// the `action` field shows 'default action', which the OS will (should) take unless
// the signal is trapped and handled differently.
// T = Abnormal termination of the process.
// A = Abnormal termination of the process (with additional actions).
// I = Ignore the signal
// S = Stop the process
// C = Continue the process, if it is stopped; otherwise, ignore the signal.
//
{ name: 'SIGABRT', action: 'A', desc: 'Process abort signal.' },
{ name: 'SIGALRM', action: 'T', desc: 'Alarm clock.' },
{ name: 'SIGBUS', action: 'A', desc: 'Access to an undefined portion of a memory object.' },
{ name: 'SIGCHLD', action: 'I', desc: 'Child process terminated, stopped, or continued. ' },
{ name: 'SIGCONT', action: 'C', desc: 'Continue executing, if stopped.' },
{ name: 'SIGFPE', action: 'A', desc: 'Erroneous arithmetic operation.' },
{ name: 'SIGHUP', action: 'T', desc: 'Hangup.' },
{ name: 'SIGILL', action: 'A', desc: 'Illegal instruction.' },
{ name: 'SIGINT', action: 'T', desc: 'Terminal interrupt signal.' },
{ name: 'SIGKILL', action: 'T', desc: 'Kill (cannot be caught or ignored).' },
{ name: 'SIGPIPE', action: 'T', desc: 'Write on a pipe with no one to read it.' },
{ name: 'SIGQUIT', action: 'A', desc: 'Terminal quit signal.' },
{ name: 'SIGSEGV', action: 'A', desc: 'Invalid memory reference.' },
{ name: 'SIGSTOP', action: 'S', desc: 'Stop executing (cannot be caught or ignored).' },
{ name: 'SIGTERM', action: 'T', desc: 'Termination signal.' },
{ name: 'SIGTSTP', action: 'S', desc: 'Terminal stop signal.' },
{ name: 'SIGTTIN', action: 'S', desc: 'Background process attempting read.' },
{ name: 'SIGTTOU', action: 'S', desc: 'Background process attempting write.' },
{ name: 'SIGUSR1', action: 'T', desc: 'User-defined signal 1.' },
{ name: 'SIGUSR2', action: 'T', desc: 'User-defined signal 2.' },
{ name: 'SIGPOLL', action: 'T', desc: 'Pollable event. ' },
{ name: 'SIGPROF', action: 'T', desc: 'Profiling timer expired. ' },
{ name: 'SIGSYS', action: 'A', desc: 'Bad system call.' },
{ name: 'SIGTRAP', action: 'A', desc: 'Trace/breakpoint trap. ' },
{ name: 'SIGURG', action: 'I', desc: 'High bandwidth data is available at a socket.' },
{ name: 'SIGVTALRM', action: 'T', desc: 'Virtual timer expired.' },
{ name: 'SIGXCPU', action: 'A', desc: 'CPU time limit exceeded.' },
{ name: 'SIGXFSZ', action: 'A', desc: 'File size limit exceeded. ' }
];
var selectedActions = {T:true, A:true},
globalResult = 0;
function finish() {
process.exit(globalResult);
}
(function testSignal(idx) {
idx = idx||0;
var sig = all_signals[idx];
if( !sig ) { finish(); return; }
function nextTest() {
process.nextTick(function() { testSignal(idx+1); });
};
// ignore signals that do not match the "selected actions"
if( ! (sig.action in selectedActions) ) {
nextTest();
return;
}
// run the test in its own process.
// the argument `sig.name` tells the script to be a "master"
// and test the given signal. (see "Master" below).
child_process.execFile(
process.execPath, [process.argv[1], sig.name], null,
function(code, stdout, stderr) {
process.stdout.write(stdout);
process.stderr.write(stderr);
globalResult = globalResult || code;
nextTest()
}
)
}());
}()); //end: toplevel
} else if( isMaster ) {
//**
//* Master (Test Stop Signal) - The first cmdline argument will be the
//* name of the signal to be tested. Test it, and exit.
//*
(function do_master() {
var excludedSignals = { 'SIGUSR1': 'used by v8/node debugger' },
testSig = process.argv[2],
tmr0 = null,
timeout = 5000,
reportText;
function clean_exit(code) {
process.removeAllListeners();
clearTimeout(tmr0);
process.exit(code);
}
process.on('uncaughtException', function (err) {
var msg = '';
if( err.name === 'AssertionError' ) {
msg = err.message;
} else {
//throw err;
msg = err;
}
if( reportText ) { msg = reportText + msg; }
print('FAIL: '+ msg + '\n');
clean_exit(1);
});
if( testSig ) {
print(testSig + ' ...');
} else {
// disable the timeout if no signal was specified...
timeout = 0;
}
if( testSig && (testSig in excludedSignals) ) {
// skip "excluded" signals (treat them as ok)
print( 'OK (skipped) ' + excludedSignals[testSig] + '\n');
clean_exit(0);
} else {
// -- run the test -- //
var didExit = false
, didDisconnect = false
if( timeout ) {
tmr0 = setTimeout( function() {
assert.ok( false, 'timeout ('+(timeout/1000)+'s): child did not respond to signal.');
}, timeout);
}
child = child_process.fork(process.argv[1]);
// child messages us, once it's up and running
child.on('message', function(msg) {
process.nextTick( start );
});
// performs the action that was requested of this test run.
// (ie. send a signal, tell child exit, or just wait)
function start() {
if( testSig ) {
child.kill(testSig);
} else {
print('master ['+process.pid+']\n');
print('child ['+child.pid+']\n');
print('waiting for child to exit... ');
}
}
child.on('disconnect', function() {
didDisconnect = true;
});
child.on('exit', function(exitCode, sigCode) {
clearTimeout(tmr0);
didExit = true;
reportText = '[exitCode='+exitCode+', signal='+sigCode+'] ';
//print(reportText);
if( testSig ) {
// verify that we got the signal we were expecting...
assert.equal( sigCode, testSig, 'exit: reported incorrect signal');
assert.equal( exitCode , null, 'exit: reported incorrect exitCode.')
} else {
assert( (exitCode != null && sigCode==null) || (exitCode == null && sigCode != null));
}
assert.ok( didDisconnect, '(exit) missing disconnect event, or events emitted out-of-order.');
});
process.on('exit', function() {
assert.ok( didExit, 'no \'exit\' event was fired for child process' );
assert.ok( didDisconnect , 'no \'disconnect\' event was fired for child process' );
print('OK\n');
});
process.on('SIGINT', function() {
print('interrupted!\n');
clean_exit(-1);
});
}
}()); //end: master
} else if( isChild ) {
//**
//* Child - created by the master, to be the recipient of the signal
//*
(function do_child() {
process.send('helo');
// child stays alive, because of ipc channel.
}()); //end: child
} else {
// should not happen.
assert.ok( false, "the test script is unable to identify whether it is "+
"master, child, or toplevel. This should never happen");
}
// a few shared utilities
function print(str) {
process.stderr.write(str);
}
@coltrane
Copy link
Author

coltrane commented May 5, 2012

The following results were obtained using node v0.7.9-pre (6f82b9f), on OS-X lion.

$ node test-child-process-signals.js 
SIGABRT ...OK
SIGALRM ...OK
SIGBUS ...OK
SIGFPE ...OK
SIGHUP ...OK
SIGILL ...OK
SIGINT ...FAIL: [exitCode=1, signal=null] exit: reported incorrect signal
SIGKILL ...OK
SIGPIPE ...FAIL: timeout (5s): child did not respond to signal.
SIGQUIT ...OK
SIGSEGV ...OK
SIGTERM ...FAIL: [exitCode=1, signal=null] exit: reported incorrect signal
SIGUSR1 ...OK (skipped) used by v8/node debugger
SIGUSR2 ...OK
SIGPOLL ...FAIL: Error: Unknown signal: SIGPOLL
SIGPROF ...OK
SIGSYS ...OK
SIGTRAP ...OK
SIGVTALRM ...OK
SIGXCPU ...OK
SIGXFSZ ...OK

Analysis:

  1. we intentionally skipped SIGUSR1 because we know that node's debugger traps this one
  2. SIGPOLL is undefined on OS-X. Darwin's headers define SIGEMT in its place. (looks like -D_POSIX_C_SOURCE might fix this, but that seems to break lots of other stuff).
  3. SIGPIPE is trapped by nodejs (src/node.cc:2723). I assume this is due to "The special problem of SIGPIPE" (see: deps/uv/src/unix/ev/ev.3).
  4. SIGINT, and SIGTERM are trapped by nodejs, and their handler (src/node.cc:2277), calls _exit(1). This causes the child_process to exit "normally" (with a non-zero exit code), rather than exiting "abnormally" (with a signal code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment