Skip to content

Commit 2fa9259

Browse files
committed
watch: enable passthrough ipc in watch mode
1 parent 8c72210 commit 2fa9259

File tree

3 files changed

+114
-7
lines changed

3 files changed

+114
-7
lines changed

lib/internal/main/watch_mode.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function start() {
6464
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
6565
}
6666
});
67+
return child;
6768
}
6869

6970
async function killAndWait(signal = kKillSignal, force = false) {
@@ -96,29 +97,31 @@ function reportGracefulTermination() {
9697
};
9798
}
9899

99-
async function stop() {
100+
async function stop(child) {
101+
// Without this line, the child process is still able to receive IPC, but is unable to send additional messages
102+
watcher.destroyIPC(child);
100103
watcher.clearFileFilters();
101104
const clearGraceReport = reportGracefulTermination();
102105
await killAndWait();
103106
clearGraceReport();
104107
}
105108

106-
async function restart() {
109+
async function restart(child) {
107110
if (!kPreserveOutput) process.stdout.write(clear);
108111
process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`);
109-
await stop();
110-
start();
112+
await stop(child);
113+
return start();
111114
}
112115

113116
(async () => {
114117
emitExperimentalWarning('Watch mode');
115-
118+
let child;
116119
try {
117-
start();
120+
child = start();
118121

119122
// eslint-disable-next-line no-unused-vars
120123
for await (const _ of on(watcher, 'changed')) {
121-
await restart();
124+
child = await restart(child);
122125
}
123126
} catch (error) {
124127
triggerUncaughtException(error, true /* fromPromise */);

lib/internal/watch_mode/files_watcher.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class FilesWatcher extends EventEmitter {
3030
#debounce;
3131
#mode;
3232
#signal;
33+
#wantsPassthroughIPC = false;
3334

3435
constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) {
3536
super();
@@ -39,6 +40,7 @@ class FilesWatcher extends EventEmitter {
3940
this.#debounce = debounce;
4041
this.#mode = mode;
4142
this.#signal = signal;
43+
this.#wantsPassthroughIPC = !!process.send;
4244

4345
if (signal) {
4446
EventEmitter.addAbortListener(signal, () => this.clear());
@@ -124,7 +126,28 @@ class FilesWatcher extends EventEmitter {
124126
this.#ownerDependencies.set(owner, dependencies);
125127
}
126128
}
129+
130+
131+
#setupIPC(child) {
132+
child._ipcMessages = {
133+
parentToChild: (message) => child.send(message),
134+
childToParent: (message) => process.send(message),
135+
};
136+
process.on('message', child._ipcMessages.parentToChild);
137+
child.on('message', child._ipcMessages.childToParent);
138+
}
139+
140+
destroyIPC(child) {
141+
if (this.#wantsPassthroughIPC) {
142+
process.off('message', child._ipcMessages.parentToChild);
143+
child.off('message', child._ipcMessages.childToParent);
144+
}
145+
}
146+
127147
watchChildProcessModules(child, key = null) {
148+
if (this.#wantsPassthroughIPC) {
149+
this.#setupIPC(child);
150+
}
128151
if (this.#mode !== 'filter') {
129152
return;
130153
}

test/sequential/test-watch-mode.mjs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { spawn } from 'node:child_process';
88
import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';
99
import { inspect } from 'node:util';
1010
import { pathToFileURL } from 'node:url';
11+
import { once } from 'node:events';
1112
import { createInterface } from 'node:readline';
1213

1314
if (common.isIBMi)
@@ -293,6 +294,7 @@ console.log(values.random);
293294
]);
294295
});
295296

297+
296298
// TODO: Remove skip after https://github.com/nodejs/node/pull/45271 lands
297299
it('should not watch when running an missing file', {
298300
skip: !supportsRecursive
@@ -356,4 +358,83 @@ console.log(values.random);
356358
`Completed running ${inspect(file)}`,
357359
]);
358360
});
361+
362+
it('should pass IPC messages from a spawning parent to the child and back', async () => {
363+
const file = createTmpFile(`console.log('running');
364+
process.on('message', (message) => {
365+
if (message === 'exit') {
366+
process.exit(0);
367+
} else {
368+
console.log('Received:', message);
369+
process.send(message);
370+
}
371+
})`);
372+
373+
const child = spawn(
374+
execPath,
375+
[
376+
'--watch',
377+
'--no-warnings',
378+
file,
379+
],
380+
{
381+
encoding: 'utf8',
382+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
383+
},
384+
);
385+
386+
let stderr = '';
387+
let stdout = '';
388+
389+
child.stdout.on('data', (data) => stdout += data);
390+
child.stderr.on('data', (data) => stderr += data);
391+
async function waitForEcho(msg) {
392+
const receivedPromise = new Promise((resolve) => {
393+
const fn = (message) => {
394+
if (message === msg) {
395+
child.off('message', fn);
396+
resolve();
397+
}
398+
};
399+
child.on('message', fn);
400+
});
401+
child.send(msg);
402+
await receivedPromise;
403+
}
404+
405+
async function waitForText(text) {
406+
const seenPromise = new Promise((resolve) => {
407+
const fn = (data) => {
408+
if (data.toString().includes(text)) {
409+
resolve();
410+
child.stdout.off('data', fn);
411+
}
412+
};
413+
child.stdout.on('data', fn);
414+
});
415+
await seenPromise;
416+
}
417+
418+
await waitForEcho('first message');
419+
const stopRestarts = restart(file);
420+
await waitForText('running');
421+
stopRestarts();
422+
await waitForEcho('second message');
423+
const exitedPromise = once(child, 'exit');
424+
child.send('exit');
425+
await waitForText('Completed');
426+
child.disconnect();
427+
child.kill();
428+
await exitedPromise;
429+
assert.strictEqual(stderr, '');
430+
const lines = stdout.split(/\r?\n/).filter(Boolean);
431+
assert.deepStrictEqual(lines, [
432+
'running',
433+
'Received: first message',
434+
`Restarting '${file}'`,
435+
'running',
436+
'Received: second message',
437+
`Completed running ${inspect(file)}`,
438+
]);
439+
});
359440
});

0 commit comments

Comments
 (0)