diff --git a/README.md b/README.md index 6d15bde5..d5a13df4 100644 --- a/README.md +++ b/README.md @@ -286,21 +286,48 @@ if (cluster.isMaster) { } ``` +### Signal handling behavior + +Nodemon implements improved signal handling for better POSIX compliance and systemd compatibility: + +- **SIGINT** (Ctrl+C): Passes signal to child process and waits for graceful exit. Press Ctrl+C twice to force immediate termination. +- **SIGTERM**: Passes signal to child process, waits for `killTimeout` (default: 10 seconds), then escalates to SIGKILL if needed. This aligns with systemd's `TimeoutStopSec` behavior. +- **SIGQUIT**: Immediately sends SIGKILL to child process for emergency shutdown without cleanup. +- **SIGHUP**: Default reload signal (changed from SIGUSR2). Triggers restart when file changes are detected or manual restart is requested. + +You can configure the timeout for graceful shutdown: + +```bash +nodemon --kill-timeout 5000 server.js +``` + +Or in `nodemon.json`: + +```json +{ + "killTimeout": 5000 +} +``` + +**Note**: The default signal changed from `SIGUSR2` to `SIGHUP` for better POSIX compliance. If your application relies on `SIGUSR2`, you can restore the old behavior by setting `--signal SIGUSR2` in your configuration. + ## Controlling shutdown of your script nodemon sends a kill signal to your application when it sees a file update. If you need to clean up on shutdown inside your script you can capture the kill signal and handle it yourself. -The following example will listen once for the `SIGUSR2` signal (used by nodemon to restart), run the clean up process and then kill itself for nodemon to continue control: +The following example will listen once for the `SIGHUP` signal (used by nodemon to restart), run the clean up process and then kill itself for nodemon to continue control: ```js // important to use `on` and not `once` as nodemon can re-send the kill signal -process.on('SIGUSR2', function () { +process.on('SIGHUP', function () { gracefulShutdown(function () { process.kill(process.pid, 'SIGTERM'); }); }); ``` +**Note**: If you're upgrading from an older version of nodemon and your application listens for `SIGUSR2`, you should either update your code to listen for `SIGHUP`, or configure nodemon to use `SIGUSR2` by setting `--signal SIGUSR2`. + Note that the `process.kill` is *only* called once your shutdown jobs are complete. Hat tip to [Benjie Gillam](http://www.benjiegillam.com/2011/08/node-js-clean-restart-and-faster-development-with-nodemon/) for writing this technique up. ## Triggering events when nodemon state changes diff --git a/lib/config/defaults.js b/lib/config/defaults.js index dc95d346..7ad7169f 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -17,7 +17,9 @@ const defaults = { stdin: true, runOnChangeOnly: false, verbose: false, - signal: 'SIGUSR2', + signal: 'SIGHUP', + // Timeout in milliseconds to wait for graceful shutdown before sending SIGKILL + killTimeout: 10000, // 'stdout' refers to the default behaviour of a required nodemon's child, // but also includes stderr. If this is false, data is still dispatched via // nodemon.on('stdout/stderr') diff --git a/lib/monitor/run.js b/lib/monitor/run.js index 5fa7f45a..2bdb518c 100644 --- a/lib/monitor/run.js +++ b/lib/monitor/run.js @@ -11,7 +11,7 @@ var watch = require('./watch').watch; var config = require('../config'); var child = null; // the actual child process we spawn var killedAfterChange = false; -var noop = () => {}; +var noop = () => { }; var restart = null; var psTree = require('pstree.remy'); var path = require('path'); @@ -105,7 +105,7 @@ function run(options) { var inBinPath = false; try { inBinPath = statSync(`${binPath}/${executable}`).isFile(); - } catch (e) {} + } catch (e) { } // hasStdio allows us to correctly handle stdin piping // see: https://git.io/vNtX3 @@ -289,7 +289,7 @@ function run(options) { // swallow the stdin error if it happens // ref: https://github.com/remy/nodemon/issues/1195 if (hasStdio) { - child.stdin.on('error', () => {}); + child.stdin.on('error', () => { }); process.stdin.pipe(child.stdin); } else { if (child.stdout) { @@ -300,7 +300,7 @@ function run(options) { ); utils.log.error( 'nodemon may not work as expected - ' + - 'please consider upgrading to LTS' + 'please consider upgrading to LTS' ); } } @@ -327,8 +327,7 @@ function waitForSubProcesses(pid, callback) { } utils.log.status( - `still waiting for ${pids.length} sub-process${ - pids.length > 2 ? 'es' : '' + `still waiting for ${pids.length} sub-process${pids.length > 2 ? 'es' : '' } to finish...` ); setTimeout(() => waitForSubProcesses(pid, callback), 1000); @@ -411,7 +410,7 @@ function kill(child, signal, callback) { // spawning processes like `coffee` under the `--debug` flag, it'll spawn // it's own child, and that can't be killed by nodemon, so psTree gives us // an array of PIDs that have spawned under nodemon, and we send each the - // configured signal (default: SIGUSR2) signal, which fixes #335 + // configured signal (default: SIGHUP) signal, which fixes #335 // note that psTree also works if `ps` is missing by looking in /proc let sig = signal.replace('SIG', ''); @@ -437,6 +436,55 @@ function kill(child, signal, callback) { } } +// Enhanced kill function with timeout support for graceful shutdown +// Implements the behavior described in issue #1705 +function killWithTimeout(child, signal, timeout, callback) { + if (!callback) { + callback = noop; + } + + if (!child) { + return callback(); + } + + const childPid = child.pid; + let hasExited = false; + let killTimer = null; + + // Set up exit handler + const onExit = () => { + if (hasExited) return; + hasExited = true; + + if (killTimer) { + clearTimeout(killTimer); + killTimer = null; + } + + debug('child process %s exited gracefully', childPid); + callback(); + }; + + // Listen for child exit + child.once('exit', onExit); + + // Send the initial signal + debug('sending %s to child process %s', signal, childPid); + kill(child, signal, () => { + // If the process hasn't exited after the timeout, escalate to SIGKILL + if (timeout > 0 && signal !== 'SIGKILL') { + killTimer = setTimeout(() => { + if (!hasExited) { + debug('child process %s did not exit after %dms, sending SIGKILL', childPid, timeout); + child.removeListener('exit', onExit); + kill(child, 'SIGKILL', callback); + } + }, timeout); + } + }); +} + + run.kill = function (noRestart, callback) { // I hate code like this :( - Remy (author of said code) if (typeof noRestart === 'function') { @@ -520,8 +568,9 @@ bus.on('quit', function onQuit(code) { config.run = false; if (child) { - // give up waiting for the kids after 10 seconds - exitTimer = setTimeout(exit, 10 * 1000); + // Use configurable timeout instead of hardcoded 10 seconds + const timeout = config.options.killTimeout || 10000; + exitTimer = setTimeout(exit, timeout); child.removeAllListeners('exit'); child.once('exit', exit); @@ -545,18 +594,91 @@ process.on('exit', function () { } }); +// Track SIGINT presses for double Ctrl+C detection +let sigintCount = 0; +let sigintTimer = null; + // because windows borks when listening for the SIG* events if (!utils.isWindows) { bus.once('boot', () => { - // usual suspect: ctrl+c exit - process.once('SIGINT', () => bus.emit('quit', 130)); + // SIGINT handler - implements graceful shutdown with double Ctrl+C for force quit + // As per issue #1705: pass signal to child, wait for timeout, second SIGINT forces quit + process.on('SIGINT', () => { + sigintCount++; + + if (sigintCount === 1) { + debug('received first SIGINT, attempting graceful shutdown'); + + // Reset counter after timeout + sigintTimer = setTimeout(() => { + sigintCount = 0; + }, config.options.killTimeout || 10000); + + if (child) { + // Pass SIGINT to child and wait for graceful exit + killWithTimeout(child, 'SIGINT', config.options.killTimeout || 10000, () => { + bus.emit('quit', 130); + }); + } else { + bus.emit('quit', 130); + } + } else if (sigintCount >= 2) { + // Second SIGINT - force quit immediately + debug('received second SIGINT, forcing immediate shutdown'); + if (sigintTimer) { + clearTimeout(sigintTimer); + } + + if (child) { + kill(child, 'SIGKILL', () => { + bus.emit('quit', 130); + }); + } else { + bus.emit('quit', 130); + } + } + }); + + // SIGTERM handler - implements graceful shutdown with timeout, then SIGKILL + // As per issue #1705: aligns with systemd's TimeoutStopSec behavior process.once('SIGTERM', () => { - bus.emit('quit', 143); + debug('received SIGTERM, attempting graceful shutdown with timeout'); + if (child) { - child.kill('SIGTERM'); + // Pass SIGTERM to child, escalate to SIGKILL after timeout + killWithTimeout(child, 'SIGTERM', config.options.killTimeout || 10000, () => { + bus.emit('quit', 143); + }); + } else { + bus.emit('quit', 143); } }); + + // SIGQUIT handler - implements immediate termination without cleanup + // As per issue #1705: immediately SIGKILL child and quit nodemon + process.once('SIGQUIT', () => { + debug('received SIGQUIT, forcing immediate termination'); + + if (child) { + kill(child, 'SIGKILL', () => { + bus.emit('quit', 131); + }); + } else { + bus.emit('quit', 131); + } + }); + + // SIGHUP handler - implements reload functionality + // As per issue #1705: use as default reload signal instead of SIGUSR2 + process.on('SIGHUP', () => { + debug('received SIGHUP, reloading'); + + // SIGHUP can either restart nodemon or be passed to child + // depending on configuration. By default, we restart. + bus.emit('restart'); + }); }); } + module.exports = run;