Skip to content

Commit fe17f5d

Browse files
dario-piotrowicztargos
authored andcommitted
watch: add --watch-kill-signal flag
add the new `--watch-kill-signal` to allow users to customize what signal is sent to the process on restarts during watch mode PR-URL: #58719 Reviewed-By: Yagiz Nizipli <[email protected]>
1 parent df85b02 commit fe17f5d

File tree

6 files changed

+147
-2
lines changed

6 files changed

+147
-2
lines changed

doc/api/cli.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3092,6 +3092,19 @@ mode. If no file is provided, Node.js will exit with status code `9`.
30923092
node --watch index.js
30933093
```
30943094

3095+
### `--watch-kill-signal`
3096+
3097+
<!-- YAML
3098+
added:
3099+
- REPLACEME
3100+
-->
3101+
3102+
Customizes the signal sent to the process on watch mode restarts.
3103+
3104+
```bash
3105+
node --watch --watch-kill-signal SIGINT test.js
3106+
```
3107+
30953108
### `--watch-path`
30963109

30973110
<!-- YAML
@@ -3434,6 +3447,7 @@ one is included in the list below.
34343447
* `--use-openssl-ca`
34353448
* `--use-system-ca`
34363449
* `--v8-pool-size`
3450+
* `--watch-kill-signal`
34373451
* `--watch-path`
34383452
* `--watch-preserve-output`
34393453
* `--watch`

doc/node-config-schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,9 @@
562562
"watch": {
563563
"type": "boolean"
564564
},
565+
"watch-kill-signal": {
566+
"type": "string"
567+
},
565568
"watch-path": {
566569
"oneOf": [
567570
{

lib/internal/main/watch_mode.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
const { getOptionValue } = require('internal/options');
2121
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
2222
const { green, blue, red, white, clear } = require('internal/util/colors');
23+
const { convertToValidSignal } = require('internal/util');
2324

2425
const { spawn } = require('child_process');
2526
const { inspect } = require('util');
@@ -30,8 +31,7 @@ const { once } = require('events');
3031
prepareMainThreadExecution(false, false);
3132
markBootstrapComplete();
3233

33-
// TODO(MoLow): Make kill signal configurable
34-
const kKillSignal = 'SIGTERM';
34+
const kKillSignal = convertToValidSignal(getOptionValue('--watch-kill-signal'));
3535
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
3636
const kEnvFile = getOptionValue('--env-file') || getOptionValue('--env-file-if-exists');
3737
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
953953
"path to watch",
954954
&EnvironmentOptions::watch_mode_paths,
955955
kAllowedInEnvvar);
956+
AddOption("--watch-kill-signal",
957+
"kill signal to send to the process on watch mode restarts"
958+
"(default: SIGTERM)",
959+
&EnvironmentOptions::watch_mode_kill_signal,
960+
kAllowedInEnvvar);
956961
AddOption("--watch-preserve-output",
957962
"preserve outputs on watch mode restart",
958963
&EnvironmentOptions::watch_mode_preserve_output,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ class EnvironmentOptions : public Options {
231231
bool watch_mode = false;
232232
bool watch_mode_report_to_parent = false;
233233
bool watch_mode_preserve_output = false;
234+
std::string watch_mode_kill_signal = "SIGTERM";
234235
std::vector<std::string> watch_mode_paths;
235236

236237
bool syntax_check_only = false;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as common from '../common/index.mjs';
2+
import { describe, it, beforeEach } from 'node:test';
3+
import { once } from 'node:events';
4+
import assert from 'node:assert';
5+
import { spawn } from 'node:child_process';
6+
import { writeFileSync } from 'node:fs';
7+
import tmpdir from '../common/tmpdir.js';
8+
9+
if (common.isWindows) {
10+
common.skip('no signals on Windows');
11+
}
12+
13+
if (common.isIBMi) {
14+
common.skip('IBMi does not support `fs.watch()`');
15+
}
16+
17+
if (common.isAIX) {
18+
common.skip('folder watch capability is limited in AIX.');
19+
}
20+
21+
const indexContents = `
22+
const { setTimeout } = require("timers/promises");
23+
(async () => {
24+
// Wait a few milliseconds to make sure that the
25+
// parent process has time to attach its listeners
26+
await setTimeout(200);
27+
28+
process.on('SIGTERM', () => {
29+
console.log('__SIGTERM received__');
30+
process.exit(123);
31+
});
32+
33+
process.on('SIGINT', () => {
34+
console.log('__SIGINT received__');
35+
process.exit(124);
36+
});
37+
38+
console.log('ready!');
39+
40+
// Wait for a long time (just to keep the process alive)
41+
await setTimeout(100_000_000);
42+
})();
43+
`;
44+
45+
let indexPath = '';
46+
47+
function refresh() {
48+
tmpdir.refresh();
49+
indexPath = tmpdir.resolve('index.js');
50+
writeFileSync(indexPath, indexContents);
51+
}
52+
53+
describe('test runner watch mode with --watch-kill-signal', () => {
54+
beforeEach(refresh);
55+
56+
it('defaults to SIGTERM', async () => {
57+
let currentRun = Promise.withResolvers();
58+
const child = spawn(process.execPath, ['--watch', indexPath], {
59+
cwd: tmpdir.path,
60+
});
61+
62+
let stdout = '';
63+
child.stdout.on('data', (data) => {
64+
stdout += data.toString();
65+
currentRun.resolve();
66+
});
67+
68+
await currentRun.promise;
69+
70+
currentRun = Promise.withResolvers();
71+
writeFileSync(indexPath, indexContents);
72+
73+
await currentRun.promise;
74+
child.kill();
75+
const [exitCode] = await once(child, 'exit');
76+
assert.match(stdout, /__SIGTERM received__/);
77+
assert.strictEqual(exitCode, 123);
78+
});
79+
80+
it('can be overridden (to SIGINT)', async () => {
81+
let currentRun = Promise.withResolvers();
82+
const child = spawn(process.execPath, ['--watch', '--watch-kill-signal', 'SIGINT', indexPath], {
83+
cwd: tmpdir.path,
84+
});
85+
let stdout = '';
86+
87+
child.stdout.on('data', (data) => {
88+
stdout += data.toString();
89+
if (stdout.includes('ready!')) {
90+
currentRun.resolve();
91+
}
92+
});
93+
94+
await currentRun.promise;
95+
96+
currentRun = Promise.withResolvers();
97+
writeFileSync(indexPath, indexContents);
98+
99+
await currentRun.promise;
100+
child.kill();
101+
const [exitCode] = await once(child, 'exit');
102+
assert.match(stdout, /__SIGINT received__/);
103+
assert.strictEqual(exitCode, 124);
104+
});
105+
106+
it('errors if an invalid signal is provided', async () => {
107+
const currentRun = Promise.withResolvers();
108+
const child = spawn(process.execPath, ['--watch', '--watch-kill-signal', 'invalid_signal', indexPath], {
109+
cwd: tmpdir.path,
110+
});
111+
let stdout = '';
112+
113+
child.stderr.on('data', (data) => {
114+
stdout += data.toString();
115+
currentRun.resolve();
116+
});
117+
118+
await currentRun.promise;
119+
120+
assert.match(stdout, new RegExp(/TypeError \[ERR_UNKNOWN_SIGNAL\]: Unknown signal: invalid_signal/));
121+
});
122+
});

0 commit comments

Comments
 (0)