Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5cbcf30

Browse files
MoLowguangwong
authored andcommittedJan 3, 2023
cli: add --watch
PR-URL: nodejs/node#44366 Backport-PR-URL: nodejs/node#44976 Fixes: nodejs/node#40429 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent f05a250 commit 5cbcf30

29 files changed

+953
-42
lines changed
 

‎doc/api/cli.md

+49
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,53 @@ on the number of online processors.
14271427
If the value provided is larger than V8's maximum, then the largest value
14281428
will be chosen.
14291429

1430+
### `--watch`
1431+
1432+
<!-- YAML
1433+
added: REPLACEME
1434+
-->
1435+
1436+
> Stability: 1 - Experimental
1437+
1438+
Starts Node.js in watch mode.
1439+
When in watch mode, changes in the watched files cause the Node.js process to
1440+
restart.
1441+
By default, watch mode will watch the entry point
1442+
and any required or imported module.
1443+
Use `--watch-path` to specify what paths to watch.
1444+
1445+
This flag cannot be combined with
1446+
`--check`, `--eval`, `--interactive`, or the REPL.
1447+
1448+
```console
1449+
$ node --watch index.js
1450+
```
1451+
1452+
### `--watch-path`
1453+
1454+
<!-- YAML
1455+
added: REPLACEME
1456+
-->
1457+
1458+
> Stability: 1 - Experimental
1459+
1460+
Starts Node.js in watch mode and specifies what paths to watch.
1461+
When in watch mode, changes in the watched paths cause the Node.js process to
1462+
restart.
1463+
This will turn off watching of required or imported modules, even when used in
1464+
combination with `--watch`.
1465+
1466+
This flag cannot be combined with
1467+
`--check`, `--eval`, `--interactive`, or the REPL.
1468+
1469+
```console
1470+
$ node --watch-path=./src --watch-path=./tests index.js
1471+
```
1472+
1473+
This option is only supported on macOS and Windows.
1474+
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
1475+
when the option is used on a platform that does not support it.
1476+
14301477
### `--zero-fill-buffers`
14311478

14321479
<!-- YAML
@@ -1724,6 +1771,8 @@ Node.js options that are allowed are:
17241771
* `--use-largepages`
17251772
* `--use-openssl-ca`
17261773
* `--v8-pool-size`
1774+
* `--watch-path`
1775+
* `--watch`
17271776
* `--zero-fill-buffers`
17281777

17291778
<!-- node-options-node end -->

‎lib/internal/assert/assertion_error.js

+17-32
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@ const { inspect } = require('internal/util/inspect');
2121
const {
2222
removeColors,
2323
} = require('internal/util');
24+
const colors = require('internal/util/colors');
2425
const {
2526
validateObject,
2627
} = require('internal/validators');
2728
const { isErrorStackTraceLimitWritable } = require('internal/errors');
2829

29-
let blue = '';
30-
let green = '';
31-
let red = '';
32-
let white = '';
3330

3431
const kReadableOperator = {
3532
deepStrictEqual: 'Expected values to be strictly deep-equal:',
@@ -169,7 +166,7 @@ function createErrDiff(actual, expected, operator) {
169166
// Only remove lines in case it makes sense to collapse those.
170167
// TODO: Accept env to always show the full error.
171168
if (actualLines.length > 50) {
172-
actualLines[46] = `${blue}...${white}`;
169+
actualLines[46] = `${colors.blue}...${colors.white}`;
173170
while (actualLines.length > 47) {
174171
ArrayPrototypePop(actualLines);
175172
}
@@ -182,7 +179,7 @@ function createErrDiff(actual, expected, operator) {
182179
// There were at least five identical lines at the end. Mark a couple of
183180
// skipped.
184181
if (i >= 5) {
185-
end = `\n${blue}...${white}${end}`;
182+
end = `\n${colors.blue}...${colors.white}${end}`;
186183
skipped = true;
187184
}
188185
if (other !== '') {
@@ -193,15 +190,15 @@ function createErrDiff(actual, expected, operator) {
193190
let printedLines = 0;
194191
let identical = 0;
195192
const msg = kReadableOperator[operator] +
196-
`\n${green}+ actual${white} ${red}- expected${white}`;
197-
const skippedMsg = ` ${blue}...${white} Lines skipped`;
193+
`\n${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
194+
const skippedMsg = ` ${colors.blue}...${colors.white} Lines skipped`;
198195

199196
let lines = actualLines;
200-
let plusMinus = `${green}+${white}`;
197+
let plusMinus = `${colors.green}+${colors.white}`;
201198
let maxLength = expectedLines.length;
202199
if (actualLines.length < maxLines) {
203200
lines = expectedLines;
204-
plusMinus = `${red}-${white}`;
201+
plusMinus = `${colors.red}-${colors.white}`;
205202
maxLength = actualLines.length;
206203
}
207204

@@ -216,7 +213,7 @@ function createErrDiff(actual, expected, operator) {
216213
res += `\n ${lines[i - 3]}`;
217214
printedLines++;
218215
} else {
219-
res += `\n${blue}...${white}`;
216+
res += `\n${colors.blue}...${colors.white}`;
220217
skipped = true;
221218
}
222219
}
@@ -272,7 +269,7 @@ function createErrDiff(actual, expected, operator) {
272269
res += `\n ${actualLines[i - 3]}`;
273270
printedLines++;
274271
} else {
275-
res += `\n${blue}...${white}`;
272+
res += `\n${colors.blue}...${colors.white}`;
276273
skipped = true;
277274
}
278275
}
@@ -286,8 +283,8 @@ function createErrDiff(actual, expected, operator) {
286283
identical = 0;
287284
// Add the actual line to the result and cache the expected diverging
288285
// line so consecutive diverging lines show up as +++--- and not +-+-+-.
289-
res += `\n${green}+${white} ${actualLine}`;
290-
other += `\n${red}-${white} ${expectedLine}`;
286+
res += `\n${colors.green}+${colors.white} ${actualLine}`;
287+
other += `\n${colors.red}-${colors.white} ${expectedLine}`;
291288
printedLines += 2;
292289
// Lines are identical
293290
} else {
@@ -306,8 +303,8 @@ function createErrDiff(actual, expected, operator) {
306303
}
307304
// Inspected object to big (Show ~50 rows max)
308305
if (printedLines > 50 && i < maxLines - 2) {
309-
return `${msg}${skippedMsg}\n${res}\n${blue}...${white}${other}\n` +
310-
`${blue}...${white}`;
306+
return `${msg}${skippedMsg}\n${res}\n${colors.blue}...${colors.white}${other}\n` +
307+
`${colors.blue}...${colors.white}`;
311308
}
312309
}
313310

@@ -347,21 +344,9 @@ class AssertionError extends Error {
347344
if (message != null) {
348345
super(String(message));
349346
} else {
350-
if (process.stderr.isTTY) {
351-
// Reset on each call to make sure we handle dynamically set environment
352-
// variables correct.
353-
if (process.stderr.hasColors()) {
354-
blue = '\u001b[34m';
355-
green = '\u001b[32m';
356-
white = '\u001b[39m';
357-
red = '\u001b[31m';
358-
} else {
359-
blue = '';
360-
green = '';
361-
white = '';
362-
red = '';
363-
}
364-
}
347+
// Reset colors on each call to make sure we handle dynamically set environment
348+
// variables correct.
349+
colors.refresh();
365350
// Prevent the error stack from being visible by duplicating the error
366351
// in a very close way to the original in case both sides are actually
367352
// instances of Error.
@@ -393,7 +378,7 @@ class AssertionError extends Error {
393378
// Only remove lines in case it makes sense to collapse those.
394379
// TODO: Accept env to always show the full error.
395380
if (res.length > 50) {
396-
res[46] = `${blue}...${white}`;
381+
res[46] = `${colors.blue}...${colors.white}`;
397382
while (res.length > 47) {
398383
ArrayPrototypePop(res);
399384
}

‎lib/internal/main/watch_mode.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict';
2+
const {
3+
ArrayPrototypeFilter,
4+
ArrayPrototypeForEach,
5+
ArrayPrototypeJoin,
6+
ArrayPrototypeMap,
7+
ArrayPrototypePushApply,
8+
ArrayPrototypeSlice,
9+
} = primordials;
10+
11+
const {
12+
prepareMainThreadExecution,
13+
} = require('internal/bootstrap/pre_execution');
14+
const { triggerUncaughtException } = internalBinding('errors');
15+
const { getOptionValue } = require('internal/options');
16+
const { emitExperimentalWarning } = require('internal/util');
17+
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
18+
const { green, blue, red, white, clear } = require('internal/util/colors');
19+
20+
const { spawn } = require('child_process');
21+
const { inspect } = require('util');
22+
const { setTimeout, clearTimeout } = require('timers');
23+
const { resolve } = require('path');
24+
const { once, on } = require('events');
25+
26+
27+
prepareMainThreadExecution(false, false);
28+
markBootstrapComplete();
29+
30+
// TODO(MoLow): Make kill signal configurable
31+
const kKillSignal = 'SIGTERM';
32+
const kShouldFilterModules = getOptionValue('--watch-path').length === 0;
33+
const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path));
34+
const kCommand = ArrayPrototypeSlice(process.argv, 1);
35+
const kCommandStr = inspect(ArrayPrototypeJoin(kCommand, ' '));
36+
const args = ArrayPrototypeFilter(process.execArgv, (arg, i, arr) =>
37+
arg !== '--watch-path' && arr[i - 1] !== '--watch-path' && arg !== '--watch');
38+
ArrayPrototypePushApply(args, kCommand);
39+
40+
const watcher = new FilesWatcher({ throttle: 500, mode: kShouldFilterModules ? 'filter' : 'all' });
41+
ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p));
42+
43+
let graceTimer;
44+
let child;
45+
let exited;
46+
47+
function start() {
48+
exited = false;
49+
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : undefined;
50+
child = spawn(process.execPath, args, { stdio, env: { ...process.env, WATCH_REPORT_DEPENDENCIES: '1' } });
51+
watcher.watchChildProcessModules(child);
52+
child.once('exit', (code) => {
53+
exited = true;
54+
if (code === 0) {
55+
process.stdout.write(`${blue}Completed running ${kCommandStr}${white}\n`);
56+
} else {
57+
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
58+
}
59+
});
60+
}
61+
62+
async function killAndWait(signal = kKillSignal, force = false) {
63+
child?.removeAllListeners();
64+
if (!child) {
65+
return;
66+
}
67+
if ((child.killed || exited) && !force) {
68+
return;
69+
}
70+
const onExit = once(child, 'exit');
71+
child.kill(signal);
72+
const { 0: exitCode } = await onExit;
73+
return exitCode;
74+
}
75+
76+
function reportGracefulTermination() {
77+
// Log if process takes more than 500ms to stop.
78+
let reported = false;
79+
clearTimeout(graceTimer);
80+
graceTimer = setTimeout(() => {
81+
reported = true;
82+
process.stdout.write(`${blue}Waiting for graceful termination...${white}\n`);
83+
}, 500).unref();
84+
return () => {
85+
clearTimeout(graceTimer);
86+
if (reported) {
87+
process.stdout.write(`${clear}${green}Gracefully restarted ${kCommandStr}${white}\n`);
88+
}
89+
};
90+
}
91+
92+
async function stop() {
93+
watcher.clearFileFilters();
94+
const clearGraceReport = reportGracefulTermination();
95+
await killAndWait();
96+
clearGraceReport();
97+
}
98+
99+
async function restart() {
100+
process.stdout.write(`${clear}${green}Restarting ${kCommandStr}${white}\n`);
101+
await stop();
102+
start();
103+
}
104+
105+
(async () => {
106+
emitExperimentalWarning('Watch mode');
107+
108+
try {
109+
start();
110+
111+
// eslint-disable-next-line no-unused-vars
112+
for await (const _ of on(watcher, 'changed')) {
113+
await restart();
114+
}
115+
} catch (error) {
116+
triggerUncaughtException(error, true /* fromPromise */);
117+
}
118+
})();
119+
120+
// Exiting gracefully to avoid stdout/stderr getting written after
121+
// parent process is killed.
122+
// this is fairly safe since user code cannot run in this process
123+
function signalHandler(signal) {
124+
return async () => {
125+
watcher.clear();
126+
const exitCode = await killAndWait(signal, true);
127+
process.exit(exitCode ?? 0);
128+
};
129+
}
130+
process.on('SIGTERM', signalHandler('SIGTERM'));
131+
process.on('SIGINT', signalHandler('SIGINT'));

‎lib/internal/modules/cjs/loader.js

+10
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const {
106106
const { getOptionValue } = require('internal/options');
107107
const preserveSymlinks = getOptionValue('--preserve-symlinks');
108108
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
109+
const shouldReportRequiredModules = process.env.WATCH_REPORT_DEPENDENCIES;
109110
// Do not eagerly grab .manifest, it may be in TDZ
110111
const policy = getOptionValue('--experimental-policy') ?
111112
require('internal/process/policy') :
@@ -186,6 +187,12 @@ function updateChildren(parent, child, scan) {
186187
ArrayPrototypePush(children, child);
187188
}
188189

190+
function reportModuleToWatchMode(filename) {
191+
if (shouldReportRequiredModules && process.send) {
192+
process.send({ 'watch:require': filename });
193+
}
194+
}
195+
189196
const moduleParentCache = new SafeWeakMap();
190197
function Module(id = '', parent) {
191198
this.id = id;
@@ -806,6 +813,7 @@ Module._load = function(request, parent, isMain) {
806813
// cache key names.
807814
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
808815
const filename = relativeResolveCache[relResolveCacheIdentifier];
816+
reportModuleToWatchMode(filename);
809817
if (filename !== undefined) {
810818
const cachedModule = Module._cache[filename];
811819
if (cachedModule !== undefined) {
@@ -858,6 +866,8 @@ Module._load = function(request, parent, isMain) {
858866
module.id = '.';
859867
}
860868

869+
reportModuleToWatchMode(filename);
870+
861871
Module._cache[filename] = module;
862872
if (parent !== undefined) {
863873
relativeResolveCache[relResolveCacheIdentifier] = filename;

‎lib/internal/modules/esm/loader.js

+4
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,10 @@ class ESMLoader {
473473
getOptionValue('--inspect-brk')
474474
);
475475

476+
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
477+
process.send({ 'watch:import': url });
478+
}
479+
476480
const job = new ModuleJob(
477481
this,
478482
url,

‎lib/internal/util/colors.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
module.exports = {
4+
blue: '',
5+
green: '',
6+
white: '',
7+
red: '',
8+
clear: '',
9+
hasColors: false,
10+
refresh() {
11+
if (process.stderr.isTTY) {
12+
const hasColors = process.stderr.hasColors();
13+
module.exports.blue = hasColors ? '\u001b[34m' : '';
14+
module.exports.green = hasColors ? '\u001b[32m' : '';
15+
module.exports.white = hasColors ? '\u001b[39m' : '';
16+
module.exports.red = hasColors ? '\u001b[31m' : '';
17+
module.exports.clear = hasColors ? '\u001bc' : '';
18+
module.exports.hasColors = hasColors;
19+
}
20+
}
21+
};
22+
23+
module.exports.refresh();
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict';
2+
3+
const {
4+
SafeMap,
5+
SafeSet,
6+
StringPrototypeStartsWith,
7+
} = primordials;
8+
9+
const { validateNumber, validateOneOf } = require('internal/validators');
10+
const { kEmptyObject } = require('internal/util');
11+
const { TIMEOUT_MAX } = require('internal/timers');
12+
13+
const EventEmitter = require('events');
14+
const { watch } = require('fs');
15+
const { fileURLToPath } = require('url');
16+
const { resolve, dirname } = require('path');
17+
const { setTimeout } = require('timers');
18+
19+
20+
const supportsRecursiveWatching = process.platform === 'win32' ||
21+
process.platform === 'darwin';
22+
23+
class FilesWatcher extends EventEmitter {
24+
#watchers = new SafeMap();
25+
#filteredFiles = new SafeSet();
26+
#throttling = new SafeSet();
27+
#throttle;
28+
#mode;
29+
30+
constructor({ throttle = 500, mode = 'filter' } = kEmptyObject) {
31+
super();
32+
33+
validateNumber(throttle, 'options.throttle', 0, TIMEOUT_MAX);
34+
validateOneOf(mode, 'options.mode', ['filter', 'all']);
35+
this.#throttle = throttle;
36+
this.#mode = mode;
37+
}
38+
39+
#isPathWatched(path) {
40+
if (this.#watchers.has(path)) {
41+
return true;
42+
}
43+
44+
for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
45+
if (watcher.recursive && StringPrototypeStartsWith(path, watchedPath)) {
46+
return true;
47+
}
48+
}
49+
50+
return false;
51+
}
52+
53+
#removeWatchedChildren(path) {
54+
for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
55+
if (path !== watchedPath && StringPrototypeStartsWith(watchedPath, path)) {
56+
this.#unwatch(watcher);
57+
this.#watchers.delete(watchedPath);
58+
}
59+
}
60+
}
61+
62+
#unwatch(watcher) {
63+
watcher.handle.removeAllListeners();
64+
watcher.handle.close();
65+
}
66+
67+
#onChange(trigger) {
68+
if (this.#throttling.has(trigger)) {
69+
return;
70+
}
71+
if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) {
72+
return;
73+
}
74+
this.#throttling.add(trigger);
75+
this.emit('changed');
76+
setTimeout(() => this.#throttling.delete(trigger), this.#throttle).unref();
77+
}
78+
79+
get watchedPaths() {
80+
return [...this.#watchers.keys()];
81+
}
82+
83+
watchPath(path, recursive = true) {
84+
if (this.#isPathWatched(path)) {
85+
return;
86+
}
87+
const watcher = watch(path, { recursive });
88+
watcher.on('change', (eventType, fileName) => this
89+
.#onChange(recursive ? resolve(path, fileName) : path));
90+
this.#watchers.set(path, { handle: watcher, recursive });
91+
if (recursive) {
92+
this.#removeWatchedChildren(path);
93+
}
94+
}
95+
96+
filterFile(file) {
97+
if (supportsRecursiveWatching) {
98+
this.watchPath(dirname(file));
99+
} else {
100+
// Having multiple FSWatcher's seems to be slower
101+
// than a single recursive FSWatcher
102+
this.watchPath(file, false);
103+
}
104+
this.#filteredFiles.add(file);
105+
}
106+
watchChildProcessModules(child) {
107+
if (this.#mode !== 'filter') {
108+
return;
109+
}
110+
child.on('message', (message) => {
111+
try {
112+
if (message['watch:require']) {
113+
this.filterFile(message['watch:require']);
114+
}
115+
if (message['watch:import']) {
116+
this.filterFile(fileURLToPath(message['watch:import']));
117+
}
118+
} catch {
119+
// Failed watching file. ignore
120+
}
121+
});
122+
}
123+
clearFileFilters() {
124+
this.#filteredFiles.clear();
125+
}
126+
clear() {
127+
this.#watchers.forEach(this.#unwatch);
128+
this.#watchers.clear();
129+
this.#filteredFiles.clear();
130+
}
131+
}
132+
133+
module.exports = { FilesWatcher };

‎node.gypi

+6
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,12 @@
317317
}],
318318
],
319319
}],
320+
[ 'coverage=="true"', {
321+
'defines': [
322+
'ALLOW_ATTACHING_DEBUGGER_IN_WATCH_MODE',
323+
'ALLOW_ATTACHING_DEBUGGER_IN_TEST_RUNNER',
324+
],
325+
}],
320326
[ 'OS=="sunos"', {
321327
'ldflags': [ '-Wl,-M,/usr/lib/ld/map.noexstk' ],
322328
}],

‎src/env-inl.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,8 @@ inline bool Environment::owns_inspector() const {
655655
}
656656

657657
inline bool Environment::should_create_inspector() const {
658-
return (flags_ & EnvironmentFlags::kNoCreateInspector) == 0;
658+
return (flags_ & EnvironmentFlags::kNoCreateInspector) == 0 &&
659+
!options_->test_runner && !options_->watch_mode;
659660
}
660661

661662
inline bool Environment::tracks_unmanaged_fds() const {

‎src/inspector_agent.cc

+3
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@ bool Agent::Start(const std::string& path,
676676
const DebugOptions& options,
677677
std::shared_ptr<ExclusiveAccess<HostPort>> host_port,
678678
bool is_main) {
679+
if (!options.allow_attaching_debugger) {
680+
return false;
681+
}
679682
path_ = path;
680683
debug_options_ = options;
681684
CHECK_NOT_NULL(host_port);

‎src/node.cc

+4
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
520520
return StartExecution(env, "internal/main/test_runner");
521521
}
522522

523+
if (env->options()->watch_mode && !first_argv.empty()) {
524+
return StartExecution(env, "internal/main/watch_mode");
525+
}
526+
523527
if (!first_argv.empty() && first_argv != "-") {
524528
return StartExecution(env, "internal/main/run_main_module");
525529
}

‎src/node_options.cc

+36-1
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,36 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
156156
errors->push_back("either --test or --interactive can be used, not both");
157157
}
158158

159+
if (watch_mode) {
160+
// TODO(MoLow): Support (incremental?) watch mode within test runner
161+
errors->push_back("either --test or --watch can be used, not both");
162+
}
163+
159164
if (debug_options_.inspector_enabled) {
160165
errors->push_back("the inspector cannot be used with --test");
161166
}
167+
#ifndef ALLOW_ATTACHING_DEBUGGER_IN_TEST_RUNNER
168+
debug_options_.allow_attaching_debugger = false;
169+
#endif
170+
}
171+
172+
if (watch_mode) {
173+
if (syntax_check_only) {
174+
errors->push_back("either --watch or --check can be used, not both");
175+
}
176+
177+
if (has_eval_string) {
178+
errors->push_back("either --watch or --eval can be used, not both");
179+
}
180+
181+
if (force_repl) {
182+
errors->push_back("either --watch or --interactive "
183+
"can be used, not both");
184+
}
185+
186+
#ifndef ALLOW_ATTACHING_DEBUGGER_IN_WATCH_MODE
187+
debug_options_.allow_attaching_debugger = false;
188+
#endif
162189
}
163190

164191
#if HAVE_INSPECTOR
@@ -580,7 +607,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
580607
"", /* undocumented, only for debugging */
581608
&EnvironmentOptions::verify_base_objects,
582609
kAllowedInEnvironment);
583-
610+
AddOption("--watch",
611+
"run in watch mode",
612+
&EnvironmentOptions::watch_mode,
613+
kAllowedInEnvironment);
614+
AddOption("--watch-path",
615+
"path to watch",
616+
&EnvironmentOptions::watch_mode_paths,
617+
kAllowedInEnvironment);
618+
Implies("--watch-path", "--watch");
584619
AddOption("--check",
585620
"syntax check script without executing",
586621
&EnvironmentOptions::syntax_check_only);

‎src/node_options.h

+5
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class DebugOptions : public Options {
7171
DebugOptions(DebugOptions&&) = default;
7272
DebugOptions& operator=(DebugOptions&&) = default;
7373

74+
bool allow_attaching_debugger = true;
7475
// --inspect
7576
bool inspector_enabled = false;
7677
// --debug
@@ -170,6 +171,10 @@ class EnvironmentOptions : public Options {
170171
false;
171172
#endif // DEBUG
172173

174+
bool watch_mode = false;
175+
bool watch_mode_report_to_parent = false;
176+
std::vector<std::string> watch_mode_paths;
177+
173178
bool syntax_check_only = false;
174179
bool has_eval_string = false;
175180
bool experimental_wasi = false;

‎test/common/inspector-helper.js

+20-8
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class InspectorSession {
151151
});
152152
}
153153

154+
154155
waitForServerDisconnect() {
155156
return this._terminationPromise;
156157
}
@@ -326,13 +327,15 @@ class InspectorSession {
326327
class NodeInstance extends EventEmitter {
327328
constructor(inspectorFlags = ['--inspect-brk=0', '--expose-internals'],
328329
scriptContents = '',
329-
scriptFile = _MAINSCRIPT) {
330+
scriptFile = _MAINSCRIPT,
331+
logger = console) {
330332
super();
331333

334+
this._logger = logger;
332335
this._scriptPath = scriptFile;
333336
this._script = scriptFile ? null : scriptContents;
334337
this._portCallback = null;
335-
this.portPromise = new Promise((resolve) => this._portCallback = resolve);
338+
this.resetPort();
336339
this._process = spawnChildProcess(inspectorFlags, scriptContents,
337340
scriptFile);
338341
this._running = true;
@@ -342,7 +345,7 @@ class NodeInstance extends EventEmitter {
342345
this._process.stdout.on('data', makeBufferingDataCallback(
343346
(line) => {
344347
this.emit('stdout', line);
345-
console.log('[out]', line);
348+
this._logger.log('[out]', line);
346349
}));
347350

348351
this._process.stderr.on('data', makeBufferingDataCallback(
@@ -351,14 +354,22 @@ class NodeInstance extends EventEmitter {
351354
this._shutdownPromise = new Promise((resolve) => {
352355
this._process.once('exit', (exitCode, signal) => {
353356
if (signal) {
354-
console.error(`[err] child process crashed, signal ${signal}`);
357+
this._logger.error(`[err] child process crashed, signal ${signal}`);
355358
}
356359
resolve({ exitCode, signal });
357360
this._running = false;
358361
});
359362
});
360363
}
361364

365+
get pid() {
366+
return this._process.pid;
367+
}
368+
369+
resetPort() {
370+
this.portPromise = new Promise((resolve) => this._portCallback = resolve);
371+
}
372+
362373
static async startViaSignal(scriptContents) {
363374
const instance = new NodeInstance(
364375
['--expose-internals'],
@@ -370,7 +381,8 @@ class NodeInstance extends EventEmitter {
370381
}
371382

372383
onStderrLine(line) {
373-
console.log('[err]', line);
384+
this.emit('stderr', line);
385+
this._logger.log('[err]', line);
374386
if (this._portCallback) {
375387
const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/);
376388
if (matches) {
@@ -387,7 +399,7 @@ class NodeInstance extends EventEmitter {
387399
}
388400

389401
httpGet(host, path, hostHeaderValue) {
390-
console.log('[test]', `Testing ${path}`);
402+
this._logger.log('[test]', `Testing ${path}`);
391403
const headers = hostHeaderValue ? { 'Host': hostHeaderValue } : null;
392404
return this.portPromise.then((port) => new Promise((resolve, reject) => {
393405
const req = http.get({ host, port, path, headers }, (res) => {
@@ -427,7 +439,7 @@ class NodeInstance extends EventEmitter {
427439
}
428440

429441
async connectInspectorSession() {
430-
console.log('[test]', 'Connecting to a child Node process');
442+
this._logger.log('[test]', 'Connecting to a child Node process');
431443
const upgradeRequest = await this.sendUpgradeRequest();
432444
return new Promise((resolve) => {
433445
upgradeRequest
@@ -438,7 +450,7 @@ class NodeInstance extends EventEmitter {
438450
}
439451

440452
async expectConnectionDeclined() {
441-
console.log('[test]', 'Checking upgrade is not possible');
453+
this._logger.log('[test]', 'Checking upgrade is not possible');
442454
const upgradeRequest = await this.sendUpgradeRequest();
443455
return new Promise((resolve) => {
444456
upgradeRequest

‎test/fixtures/watch-mode/dependant.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const dependency = require('./dependency');
2+
console.log(dependency);
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import dependency from './dependency.mjs';
2+
console.log(dependency);
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};

‎test/fixtures/watch-mode/failing.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error('fails');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
setInterval(() => {}, 1000);
3+
console.log('running');
4+
5+
process.on('SIGTERM', () => {
6+
setTimeout(() => {
7+
console.log('exiting gracefully');
8+
process.exit(0);
9+
}, 1000);
10+
});
11+
12+
process.on('SIGINT', () => {
13+
setTimeout(() => {
14+
console.log('exiting gracefully');
15+
process.exit(0);
16+
}, 1000);
17+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log('running');
2+
while(true) {};

‎test/fixtures/watch-mode/inspect.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log('safe to debug now');
2+
setInterval(() => {}, 1000);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log('pid is', process.pid);
2+
setInterval(() => {}, 1000);

‎test/fixtures/watch-mode/ipc.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const path = require('node:path');
2+
const url = require('node:url');
3+
const os = require('node:os');
4+
const fs = require('node:fs');
5+
6+
const tmpfile = path.join(os.tmpdir(), 'file');
7+
fs.writeFileSync(tmpfile, '');
8+
9+
process.send({ 'watch:require': path.resolve(__filename) });
10+
process.send({ 'watch:import': url.pathToFileURL(path.resolve(__filename)).toString() });
11+
process.send({ 'watch:import': url.pathToFileURL(tmpfile).toString() });
12+
process.send({ 'watch:import': new URL('http://invalid.com').toString() });
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const { parseArgs } = require('node:util');
2+
3+
const { values } = parseArgs({ options: { random: { type: 'string' } } });
4+
console.log(values.random);
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
setImmediate(() => process.exit(0));
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Flags: --expose-internals
2+
import * as common from '../common/index.mjs';
3+
import * as fixtures from '../common/fixtures.mjs';
4+
import tmpdir from '../common/tmpdir.js';
5+
import path from 'node:path';
6+
import assert from 'node:assert';
7+
import process from 'node:process';
8+
import os from 'node:os';
9+
import { describe, it, beforeEach, afterEach } from 'node:test';
10+
import { writeFileSync, mkdirSync } from 'node:fs';
11+
import { setTimeout } from 'node:timers/promises';
12+
import { once } from 'node:events';
13+
import { spawn } from 'node:child_process';
14+
import watcher from 'internal/watch_mode/files_watcher';
15+
16+
if (common.isIBMi)
17+
common.skip('IBMi does not support `fs.watch()`');
18+
19+
const supportsRecursiveWatching = common.isOSX || common.isWindows;
20+
21+
const { FilesWatcher } = watcher;
22+
tmpdir.refresh();
23+
24+
describe('watch mode file watcher', () => {
25+
let watcher;
26+
let changesCount;
27+
28+
beforeEach(() => {
29+
changesCount = 0;
30+
watcher = new FilesWatcher({ throttle: 100 });
31+
watcher.on('changed', () => changesCount++);
32+
});
33+
34+
afterEach(() => watcher.clear());
35+
36+
let counter = 0;
37+
function writeAndWaitForChanges(watcher, file) {
38+
return new Promise((resolve) => {
39+
const interval = setInterval(() => writeFileSync(file, `write ${counter++}`), 100);
40+
watcher.once('changed', () => {
41+
clearInterval(interval);
42+
resolve();
43+
});
44+
});
45+
}
46+
47+
it('should watch changed files', async () => {
48+
const file = path.join(tmpdir.path, 'file1');
49+
writeFileSync(file, 'written');
50+
watcher.filterFile(file);
51+
await writeAndWaitForChanges(watcher, file);
52+
assert.strictEqual(changesCount, 1);
53+
});
54+
55+
it('should throttle changes', async () => {
56+
const file = path.join(tmpdir.path, 'file2');
57+
writeFileSync(file, 'written');
58+
watcher.filterFile(file);
59+
await writeAndWaitForChanges(watcher, file);
60+
61+
writeFileSync(file, '1');
62+
writeFileSync(file, '2');
63+
writeFileSync(file, '3');
64+
writeFileSync(file, '4');
65+
await setTimeout(200); // throttle * 2
66+
writeFileSync(file, '5');
67+
const changed = once(watcher, 'changed');
68+
writeFileSync(file, 'after');
69+
await changed;
70+
// Unfortunately testing that changesCount === 2 is flaky
71+
assert.ok(changesCount < 5);
72+
});
73+
74+
it('should ignore files in watched directory if they are not filtered',
75+
{ skip: !supportsRecursiveWatching }, async () => {
76+
watcher.on('changed', common.mustNotCall());
77+
watcher.watchPath(tmpdir.path);
78+
writeFileSync(path.join(tmpdir.path, 'file3'), '1');
79+
// Wait for this long to make sure changes are not triggered
80+
await setTimeout(1000);
81+
});
82+
83+
it('should allow clearing filters', async () => {
84+
const file = path.join(tmpdir.path, 'file4');
85+
writeFileSync(file, 'written');
86+
watcher.filterFile(file);
87+
await writeAndWaitForChanges(watcher, file);
88+
89+
writeFileSync(file, '1');
90+
91+
await setTimeout(200); // avoid throttling
92+
watcher.clearFileFilters();
93+
writeFileSync(file, '2');
94+
// Wait for this long to make sure changes are triggered only once
95+
await setTimeout(1000);
96+
assert.strictEqual(changesCount, 1);
97+
});
98+
99+
it('should watch all files in watched path when in "all" mode',
100+
{ skip: !supportsRecursiveWatching }, async () => {
101+
watcher = new FilesWatcher({ throttle: 100, mode: 'all' });
102+
watcher.on('changed', () => changesCount++);
103+
104+
const file = path.join(tmpdir.path, 'file5');
105+
watcher.watchPath(tmpdir.path);
106+
107+
const changed = once(watcher, 'changed');
108+
writeFileSync(file, 'changed');
109+
await changed;
110+
assert.strictEqual(changesCount, 1);
111+
});
112+
113+
it('should ruse existing watcher if it exists',
114+
{ skip: !supportsRecursiveWatching }, () => {
115+
assert.deepStrictEqual(watcher.watchedPaths, []);
116+
watcher.watchPath(tmpdir.path);
117+
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
118+
watcher.watchPath(tmpdir.path);
119+
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
120+
});
121+
122+
it('should ruse existing watcher of a parent directory',
123+
{ skip: !supportsRecursiveWatching }, () => {
124+
assert.deepStrictEqual(watcher.watchedPaths, []);
125+
watcher.watchPath(tmpdir.path);
126+
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
127+
watcher.watchPath(path.join(tmpdir.path, 'subdirectory'));
128+
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
129+
});
130+
131+
it('should remove existing watcher if adding a parent directory watcher',
132+
{ skip: !supportsRecursiveWatching }, () => {
133+
assert.deepStrictEqual(watcher.watchedPaths, []);
134+
const subdirectory = path.join(tmpdir.path, 'subdirectory');
135+
mkdirSync(subdirectory);
136+
watcher.watchPath(subdirectory);
137+
assert.deepStrictEqual(watcher.watchedPaths, [subdirectory]);
138+
watcher.watchPath(tmpdir.path);
139+
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
140+
});
141+
142+
it('should clear all watchers when calling clear',
143+
{ skip: !supportsRecursiveWatching }, () => {
144+
assert.deepStrictEqual(watcher.watchedPaths, []);
145+
watcher.watchPath(tmpdir.path);
146+
assert.deepStrictEqual(watcher.watchedPaths, [tmpdir.path]);
147+
watcher.clear();
148+
assert.deepStrictEqual(watcher.watchedPaths, []);
149+
});
150+
151+
it('should watch files from subprocess IPC events', async () => {
152+
const file = fixtures.path('watch-mode/ipc.js');
153+
const child = spawn(process.execPath, [file], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], encoding: 'utf8' });
154+
watcher.watchChildProcessModules(child);
155+
await once(child, 'exit');
156+
let expected = [file, path.join(os.tmpdir(), 'file')];
157+
if (supportsRecursiveWatching) {
158+
expected = expected.map((file) => path.dirname(file));
159+
}
160+
assert.deepStrictEqual(watcher.watchedPaths, expected);
161+
});
162+
});

‎test/parallel/test-watch-mode.mjs

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import * as common from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import tmpdir from '../common/tmpdir.js';
4+
import assert from 'node:assert';
5+
import path from 'node:path';
6+
import { execPath } from 'node:process';
7+
import { describe, it } from 'node:test';
8+
import { spawn } from 'node:child_process';
9+
import { writeFileSync, readFileSync } from 'node:fs';
10+
import { inspect } from 'node:util';
11+
import { once } from 'node:events';
12+
import { setTimeout } from 'node:timers/promises';
13+
import { NodeInstance } from '../common/inspector-helper.js';
14+
15+
16+
if (common.isIBMi)
17+
common.skip('IBMi does not support `fs.watch()`');
18+
19+
async function spawnWithRestarts({
20+
args,
21+
file,
22+
restarts,
23+
startedPredicate,
24+
restartMethod,
25+
}) {
26+
args ??= [file];
27+
const printedArgs = inspect(args.slice(args.indexOf(file)).join(' '));
28+
startedPredicate ??= (data) => Boolean(data.match(new RegExp(`(Failed|Completed) running ${printedArgs.replace(/\\/g, '\\\\')}`, 'g'))?.length);
29+
restartMethod ??= () => writeFileSync(file, readFileSync(file));
30+
31+
let stderr = '';
32+
let stdout = '';
33+
let restartCount = 0;
34+
let completedStart = false;
35+
let finished = false;
36+
37+
const child = spawn(execPath, ['--watch', '--no-warnings', ...args], { encoding: 'utf8' });
38+
child.stderr.on('data', (data) => {
39+
stderr += data;
40+
});
41+
child.stdout.on('data', async (data) => {
42+
if (finished) return;
43+
stdout += data;
44+
const restartMessages = stdout.match(new RegExp(`Restarting ${printedArgs.replace(/\\/g, '\\\\')}`, 'g'))?.length ?? 0;
45+
completedStart = completedStart || startedPredicate(data.toString());
46+
if (restartMessages >= restarts && completedStart) {
47+
finished = true;
48+
child.kill();
49+
return;
50+
}
51+
if (restartCount <= restartMessages && completedStart) {
52+
await setTimeout(restartCount > 0 ? 1000 : 50, { ref: false }); // Prevent throttling
53+
restartCount++;
54+
completedStart = false;
55+
restartMethod();
56+
}
57+
});
58+
59+
await Promise.race([once(child, 'exit'), once(child, 'error')]);
60+
return { stderr, stdout };
61+
}
62+
63+
let tmpFiles = 0;
64+
function createTmpFile(content = 'console.log("running");') {
65+
const file = path.join(tmpdir.path, `${tmpFiles++}.js`);
66+
writeFileSync(file, content);
67+
return file;
68+
}
69+
70+
function removeGraceMessage(stdout, file) {
71+
// Remove the message in case restart took long to avoid flakiness
72+
return stdout
73+
.replaceAll('Waiting for graceful termination...', '')
74+
.replaceAll(`Gracefully restarted ${inspect(file)}`, '');
75+
}
76+
77+
tmpdir.refresh();
78+
79+
// Warning: this suite can run safely with concurrency: true
80+
// only if tests do not watch/depend on the same files
81+
describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
82+
it('should watch changes to a file - event loop ended', async () => {
83+
const file = createTmpFile();
84+
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 1 });
85+
86+
assert.strictEqual(stderr, '');
87+
assert.strictEqual(removeGraceMessage(stdout, file), [
88+
'running', `Completed running ${inspect(file)}`, `Restarting ${inspect(file)}`,
89+
'running', `Completed running ${inspect(file)}`, '',
90+
].join('\n'));
91+
});
92+
93+
it('should watch changes to a failing file', async () => {
94+
const file = fixtures.path('watch-mode/failing.js');
95+
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 1 });
96+
97+
assert.match(stderr, /Error: fails\r?\n/);
98+
assert.strictEqual(stderr.match(/Error: fails\r?\n/g).length, 2);
99+
assert.strictEqual(removeGraceMessage(stdout, file), [`Failed running ${inspect(file)}`, `Restarting ${inspect(file)}`,
100+
`Failed running ${inspect(file)}`, ''].join('\n'));
101+
});
102+
103+
it('should not watch when running an non-existing file', async () => {
104+
const file = fixtures.path('watch-mode/non-existing.js');
105+
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 0, restartMethod: () => {} });
106+
107+
assert.match(stderr, /code: 'MODULE_NOT_FOUND'/);
108+
assert.strictEqual(stdout, [`Failed running ${inspect(file)}`, ''].join('\n'));
109+
});
110+
111+
it('should watch when running an non-existing file - when specified under --watch-path', {
112+
skip: !common.isOSX && !common.isWindows
113+
}, async () => {
114+
const file = fixtures.path('watch-mode/subdir/non-existing.js');
115+
const watched = fixtures.path('watch-mode/subdir/file.js');
116+
const { stderr, stdout } = await spawnWithRestarts({
117+
file,
118+
args: ['--watch-path', fixtures.path('./watch-mode/subdir/'), file],
119+
restarts: 1,
120+
restartMethod: () => writeFileSync(watched, readFileSync(watched))
121+
});
122+
123+
assert.strictEqual(stderr, '');
124+
assert.strictEqual(removeGraceMessage(stdout, file), [`Failed running ${inspect(file)}`, `Restarting ${inspect(file)}`,
125+
`Failed running ${inspect(file)}`, ''].join('\n'));
126+
});
127+
128+
it('should watch changes to a file - event loop blocked', async () => {
129+
const file = fixtures.path('watch-mode/infinite-loop.js');
130+
const { stderr, stdout } = await spawnWithRestarts({
131+
file,
132+
restarts: 2,
133+
startedPredicate: (data) => data.startsWith('running'),
134+
});
135+
136+
assert.strictEqual(stderr, '');
137+
assert.strictEqual(removeGraceMessage(stdout, file),
138+
['running', `Restarting ${inspect(file)}`, 'running', `Restarting ${inspect(file)}`, 'running', ''].join('\n'));
139+
});
140+
141+
it('should watch changes to dependencies - cjs', async () => {
142+
const file = fixtures.path('watch-mode/dependant.js');
143+
const dependency = fixtures.path('watch-mode/dependency.js');
144+
const { stderr, stdout } = await spawnWithRestarts({
145+
file,
146+
restarts: 1,
147+
restartMethod: () => writeFileSync(dependency, readFileSync(dependency)),
148+
});
149+
150+
assert.strictEqual(stderr, '');
151+
assert.strictEqual(removeGraceMessage(stdout, file), [
152+
'{}', `Completed running ${inspect(file)}`, `Restarting ${inspect(file)}`,
153+
'{}', `Completed running ${inspect(file)}`, '',
154+
].join('\n'));
155+
});
156+
157+
it('should watch changes to dependencies - esm', async () => {
158+
const file = fixtures.path('watch-mode/dependant.mjs');
159+
const dependency = fixtures.path('watch-mode/dependency.mjs');
160+
const { stderr, stdout } = await spawnWithRestarts({
161+
file,
162+
restarts: 1,
163+
restartMethod: () => writeFileSync(dependency, readFileSync(dependency)),
164+
});
165+
166+
assert.strictEqual(stderr, '');
167+
assert.strictEqual(removeGraceMessage(stdout, file), [
168+
'{}', `Completed running ${inspect(file)}`, `Restarting ${inspect(file)}`,
169+
'{}', `Completed running ${inspect(file)}`, '',
170+
].join('\n'));
171+
});
172+
173+
it('should restart multiple times', async () => {
174+
const file = createTmpFile();
175+
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 3 });
176+
177+
assert.strictEqual(stderr, '');
178+
assert.strictEqual(stdout.match(new RegExp(`Restarting ${inspect(file).replace(/\\/g, '\\\\')}`, 'g')).length, 3);
179+
});
180+
181+
it('should gracefully wait when restarting', { skip: common.isWindows }, async () => {
182+
const file = fixtures.path('watch-mode/graceful-sigterm.js');
183+
const { stderr, stdout } = await spawnWithRestarts({
184+
file,
185+
restarts: 1,
186+
startedPredicate: (data) => data.startsWith('running'),
187+
});
188+
189+
// This message appearing is very flaky depending on a race between the
190+
// inner process and the outer process. it is acceptable for the message not to appear
191+
// as long as the SIGTERM handler is respected.
192+
if (stdout.includes('Waiting for graceful termination...')) {
193+
assert.strictEqual(stdout, ['running', `Restarting ${inspect(file)}`, 'Waiting for graceful termination...',
194+
'exiting gracefully', `Gracefully restarted ${inspect(file)}`, 'running', ''].join('\n'));
195+
} else {
196+
assert.strictEqual(stdout, ['running', `Restarting ${inspect(file)}`, 'exiting gracefully', 'running', ''].join('\n'));
197+
}
198+
assert.strictEqual(stderr, '');
199+
});
200+
201+
it('should pass arguments to file', async () => {
202+
const file = fixtures.path('watch-mode/parse_args.js');
203+
const random = Date.now().toString();
204+
const args = [file, '--random', random];
205+
const { stderr, stdout } = await spawnWithRestarts({ file, args, restarts: 1 });
206+
207+
assert.strictEqual(stderr, '');
208+
assert.strictEqual(removeGraceMessage(stdout, args.join(' ')), [
209+
random, `Completed running ${inspect(args.join(' '))}`, `Restarting ${inspect(args.join(' '))}`,
210+
random, `Completed running ${inspect(args.join(' '))}`, '',
211+
].join('\n'));
212+
});
213+
214+
it('should not load --require modules in main process', async () => {
215+
const file = createTmpFile('');
216+
const required = fixtures.path('watch-mode/process_exit.js');
217+
const args = ['--require', required, file];
218+
const { stderr, stdout } = await spawnWithRestarts({ file, args, restarts: 1 });
219+
220+
assert.strictEqual(stderr, '');
221+
assert.strictEqual(removeGraceMessage(stdout, file), [
222+
`Completed running ${inspect(file)}`, `Restarting ${inspect(file)}`, `Completed running ${inspect(file)}`, '',
223+
].join('\n'));
224+
});
225+
226+
it('should not load --import modules in main process', async () => {
227+
const file = createTmpFile('');
228+
const imported = fixtures.fileURL('watch-mode/process_exit.js');
229+
const args = ['--import', imported, file];
230+
const { stderr, stdout } = await spawnWithRestarts({ file, args, restarts: 1 });
231+
232+
assert.strictEqual(stderr, '');
233+
assert.strictEqual(removeGraceMessage(stdout, file), [
234+
`Completed running ${inspect(file)}`, `Restarting ${inspect(file)}`, `Completed running ${inspect(file)}`, '',
235+
].join('\n'));
236+
});
237+
238+
describe('inspect', {
239+
skip: Boolean(process.config.variables.coverage || !process.features.inspector),
240+
}, () => {
241+
const silentLogger = { log: () => {}, error: () => {} };
242+
async function getDebuggedPid(instance, waitForLog = true) {
243+
const session = await instance.connectInspectorSession();
244+
await session.send({ method: 'Runtime.enable' });
245+
if (waitForLog) {
246+
await session.waitForConsoleOutput('log', 'safe to debug now');
247+
}
248+
const { value: innerPid } = (await session.send({
249+
'method': 'Runtime.evaluate', 'params': { 'expression': 'process.pid' }
250+
})).result;
251+
session.disconnect();
252+
return innerPid;
253+
}
254+
255+
it('should start debugger on inner process', async () => {
256+
const file = fixtures.path('watch-mode/inspect.js');
257+
const instance = new NodeInstance(['--inspect=0', '--watch'], undefined, file, silentLogger);
258+
let stderr = '';
259+
instance.on('stderr', (data) => { stderr += data; });
260+
261+
const pids = [instance.pid];
262+
pids.push(await getDebuggedPid(instance));
263+
instance.resetPort();
264+
writeFileSync(file, readFileSync(file));
265+
pids.push(await getDebuggedPid(instance));
266+
267+
await instance.kill();
268+
269+
// There should be 3 pids (one parent + 2 restarts).
270+
// Message about Debugger should only appear twice.
271+
assert.strictEqual(stderr.match(/Debugger listening on ws:\/\//g).length, 2);
272+
assert.strictEqual(new Set(pids).size, 3);
273+
});
274+
275+
it('should prevent attaching debugger with SIGUSR1 to outer process', { skip: common.isWindows }, async () => {
276+
const file = fixtures.path('watch-mode/inspect_with_signal.js');
277+
const instance = new NodeInstance(['--inspect-port=0', '--watch'], undefined, file, silentLogger);
278+
let stderr = '';
279+
instance.on('stderr', (data) => { stderr += data; });
280+
281+
const loggedPid = await new Promise((resolve) => {
282+
instance.on('stdout', (data) => {
283+
const matches = data.match(/pid is (\d+)/);
284+
if (matches) resolve(Number(matches[1]));
285+
});
286+
});
287+
288+
289+
process.kill(instance.pid, 'SIGUSR1');
290+
process.kill(loggedPid, 'SIGUSR1');
291+
const debuggedPid = await getDebuggedPid(instance, false);
292+
293+
await instance.kill();
294+
295+
// Message about Debugger should only appear once in inner process.
296+
assert.strictEqual(stderr.match(/Debugger listening on ws:\/\//g).length, 1);
297+
assert.strictEqual(loggedPid, debuggedPid);
298+
});
299+
});
300+
});

0 commit comments

Comments
 (0)
Please sign in to comment.