Skip to content

Commit e9e40d5

Browse files
committed
watch: remove NODE_OPTIONS from spawn call to prevent infinite loop
1 parent 2de3482 commit e9e40d5

File tree

2 files changed

+80
-5
lines changed

2 files changed

+80
-5
lines changed

lib/internal/main/watch_mode.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,15 @@ let exited;
9595
function start() {
9696
exited = false;
9797
const stdio = kShouldFilterModules ? ['inherit', 'inherit', 'inherit', 'ipc'] : 'inherit';
98+
const env = {
99+
...process.env,
100+
WATCH_REPORT_DEPENDENCIES: '1',
101+
};
102+
103+
delete env.NODE_OPTIONS;
98104
child = spawn(process.execPath, argsWithoutWatchOptions, {
99105
stdio,
100-
env: {
101-
...process.env,
102-
WATCH_REPORT_DEPENDENCIES: '1',
103-
},
106+
env,
104107
});
105108
watcher.watchChildProcessModules(child);
106109
if (kEnvFiles.length > 0) {

test/sequential/test-watch-mode-watch-flags.mjs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from 'node:path';
55
import { execPath } from 'node:process';
66
import { describe, it } from 'node:test';
77
import { spawn } from 'node:child_process';
8-
import { writeFileSync, mkdirSync } from 'node:fs';
8+
import { writeFileSync, mkdirSync, chmodSync } from 'node:fs';
99
import { inspect } from 'node:util';
1010
import { createInterface } from 'node:readline';
1111

@@ -48,6 +48,44 @@ async function runNode({
4848
return { stdout, stderr, pid: child.pid };
4949
}
5050

51+
async function runExecutable({
52+
file,
53+
expectedCompletionLog = 'Completed running',
54+
options = {},
55+
timeout = common.platformTimeout(10_000),
56+
}) {
57+
const child = spawn(file, [], { encoding: 'utf8', stdio: 'pipe', ...options });
58+
let stderr = '';
59+
const stdout = [];
60+
let timedOut = true;
61+
62+
child.stderr.on('data', (data) => {
63+
stderr += data;
64+
});
65+
66+
const timer = setTimeout(() => {
67+
child.kill();
68+
}, timeout);
69+
70+
try {
71+
for await (const data of createInterface({ input: child.stdout })) {
72+
if (!data.startsWith('Waiting for graceful termination') &&
73+
!data.startsWith('Gracefully restarted')) {
74+
stdout.push(data);
75+
}
76+
if (data.startsWith(expectedCompletionLog)) {
77+
timedOut = false;
78+
break;
79+
}
80+
}
81+
} finally {
82+
clearTimeout(timer);
83+
child.kill();
84+
}
85+
86+
return { stdout, stderr, timedOut };
87+
}
88+
5189
tmpdir.refresh();
5290

5391
describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL, timeout: 60_000 }, () => {
@@ -94,4 +132,38 @@ describe('watch mode - watch flags', { concurrency: !process.env.TEST_PARALLEL,
94132
`Completed running ${inspect(file)}. Waiting for file changes before restarting...`,
95133
]);
96134
});
135+
136+
it('should not recursively re-enter watch mode for shebang scripts when NODE_OPTIONS=--watch',
137+
{ skip: common.isWindows || process.config.variables.node_without_node_options },
138+
async () => {
139+
const projectDir = tmpdir.resolve('project-watch-node-options-shebang');
140+
mkdirSync(projectDir);
141+
142+
const file = createTmpFile(
143+
'#!/usr/bin/env node\nconsole.log("shebang run");\n',
144+
'.js',
145+
projectDir,
146+
);
147+
chmodSync(file, 0o755);
148+
149+
const { stdout, stderr, timedOut } = await runExecutable({
150+
file,
151+
options: {
152+
cwd: projectDir,
153+
env: {
154+
...process.env,
155+
NODE_OPTIONS: '--watch',
156+
// Ensure shebang resolves this test binary, not system node.
157+
PATH: `${path.dirname(execPath)}${path.delimiter}${process.env.PATH ?? ''}`,
158+
},
159+
},
160+
});
161+
162+
assert.strictEqual(timedOut, false);
163+
assert.strictEqual(stderr, '');
164+
assert.deepStrictEqual(stdout, [
165+
'shebang run',
166+
`Completed running ${inspect(file)}. Waiting for file changes before restarting...`,
167+
]);
168+
});
97169
});

0 commit comments

Comments
 (0)