Skip to content

Commit e895f7c

Browse files
znewshamtargos
authored andcommitted
watch: enable passthrough ipc in watch mode
PR-URL: #50890 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 37098eb commit e895f7c

File tree

3 files changed

+131
-11
lines changed

3 files changed

+131
-11
lines changed

lib/internal/main/watch_mode.js

+21-11
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function start() {
8181
process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`);
8282
}
8383
});
84+
return child;
8485
}
8586

8687
async function killAndWait(signal = kKillSignal, force = false) {
@@ -113,34 +114,43 @@ function reportGracefulTermination() {
113114
};
114115
}
115116

116-
async function stop() {
117+
async function stop(child) {
118+
// Without this line, the child process is still able to receive IPC, but is unable to send additional messages
119+
watcher.destroyIPC(child);
117120
watcher.clearFileFilters();
118121
const clearGraceReport = reportGracefulTermination();
119122
await killAndWait();
120123
clearGraceReport();
121124
}
122125

123126
let restarting = false;
124-
async function restart() {
127+
async function restart(child) {
125128
if (restarting) return;
126129
restarting = true;
127130
try {
128131
if (!kPreserveOutput) process.stdout.write(clear);
129132
process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`);
130-
await stop();
131-
start();
133+
await stop(child);
134+
return start();
132135
} finally {
133136
restarting = false;
134137
}
135138
}
136139

137-
start();
138-
watcher
139-
.on('changed', restart)
140-
.on('error', (error) => {
141-
watcher.off('changed', restart);
142-
triggerUncaughtException(error, true /* fromPromise */);
143-
});
140+
async function init() {
141+
let child = start();
142+
const restartChild = async () => {
143+
child = await restart(child);
144+
};
145+
watcher
146+
.on('changed', restartChild)
147+
.on('error', (error) => {
148+
watcher.off('changed', restartChild);
149+
triggerUncaughtException(error, true /* fromPromise */);
150+
});
151+
}
152+
153+
init();
144154

145155
// Exiting gracefully to avoid stdout/stderr getting written after
146156
// parent process is killed.

lib/internal/watch_mode/files_watcher.js

+29
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
const {
44
ArrayIsArray,
55
ArrayPrototypeForEach,
6+
Boolean,
67
SafeMap,
78
SafeSet,
9+
SafeWeakMap,
810
StringPrototypeStartsWith,
911
} = primordials;
1012

@@ -31,6 +33,8 @@ class FilesWatcher extends EventEmitter {
3133
#debounce;
3234
#mode;
3335
#signal;
36+
#passthroughIPC = false;
37+
#ipcHandlers = new SafeWeakMap();
3438

3539
constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) {
3640
super({ __proto__: null, captureRejections: true });
@@ -40,6 +44,7 @@ class FilesWatcher extends EventEmitter {
4044
this.#debounce = debounce;
4145
this.#mode = mode;
4246
this.#signal = signal;
47+
this.#passthroughIPC = Boolean(process.send);
4348

4449
if (signal) {
4550
addAbortListener(signal, () => this.clear());
@@ -128,7 +133,31 @@ class FilesWatcher extends EventEmitter {
128133
this.#ownerDependencies.set(owner, dependencies);
129134
}
130135
}
136+
137+
138+
#setupIPC(child) {
139+
const handlers = {
140+
__proto__: null,
141+
parentToChild: (message) => child.send(message),
142+
childToParent: (message) => process.send(message),
143+
};
144+
this.#ipcHandlers.set(child, handlers);
145+
process.on('message', handlers.parentToChild);
146+
child.on('message', handlers.childToParent);
147+
}
148+
149+
destroyIPC(child) {
150+
const handlers = this.#ipcHandlers.get(child);
151+
if (this.#passthroughIPC && handlers !== undefined) {
152+
process.off('message', handlers.parentToChild);
153+
child.off('message', handlers.childToParent);
154+
}
155+
}
156+
131157
watchChildProcessModules(child, key = null) {
158+
if (this.#passthroughIPC) {
159+
this.#setupIPC(child);
160+
}
132161
if (this.#mode !== 'filter') {
133162
return;
134163
}

test/sequential/test-watch-mode.mjs

+81
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)
@@ -574,4 +575,84 @@ console.log(values.random);
574575
`Completed running ${inspect(file)}`,
575576
]);
576577
});
578+
579+
it('should pass IPC messages from a spawning parent to the child and back', async () => {
580+
const file = createTmpFile(`console.log('running');
581+
process.on('message', (message) => {
582+
if (message === 'exit') {
583+
process.exit(0);
584+
} else {
585+
console.log('Received:', message);
586+
process.send(message);
587+
}
588+
})`);
589+
590+
const child = spawn(
591+
execPath,
592+
[
593+
'--watch',
594+
'--no-warnings',
595+
file,
596+
],
597+
{
598+
encoding: 'utf8',
599+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
600+
},
601+
);
602+
603+
let stderr = '';
604+
let stdout = '';
605+
606+
child.stdout.on('data', (data) => stdout += data);
607+
child.stderr.on('data', (data) => stderr += data);
608+
async function waitForEcho(msg) {
609+
const receivedPromise = new Promise((resolve) => {
610+
const fn = (message) => {
611+
if (message === msg) {
612+
child.off('message', fn);
613+
resolve();
614+
}
615+
};
616+
child.on('message', fn);
617+
});
618+
child.send(msg);
619+
await receivedPromise;
620+
}
621+
622+
async function waitForText(text) {
623+
const seenPromise = new Promise((resolve) => {
624+
const fn = (data) => {
625+
if (data.toString().includes(text)) {
626+
resolve();
627+
child.stdout.off('data', fn);
628+
}
629+
};
630+
child.stdout.on('data', fn);
631+
});
632+
await seenPromise;
633+
}
634+
635+
await waitForText('running');
636+
await waitForEcho('first message');
637+
const stopRestarts = restart(file);
638+
await waitForText('running');
639+
stopRestarts();
640+
await waitForEcho('second message');
641+
const exitedPromise = once(child, 'exit');
642+
child.send('exit');
643+
await waitForText('Completed');
644+
child.disconnect();
645+
child.kill();
646+
await exitedPromise;
647+
assert.strictEqual(stderr, '');
648+
const lines = stdout.split(/\r?\n/).filter(Boolean);
649+
assert.deepStrictEqual(lines, [
650+
'running',
651+
'Received: first message',
652+
`Restarting ${inspect(file)}`,
653+
'running',
654+
'Received: second message',
655+
`Completed running ${inspect(file)}`,
656+
]);
657+
});
577658
});

0 commit comments

Comments
 (0)