Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
148 changes: 135 additions & 13 deletions lib/monitor/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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'
);
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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', '');

Expand All @@ -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') {
Expand Down Expand Up @@ -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);

Expand All @@ -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;