Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e895f7c

Browse filesBrowse 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
Copy full SHA for e895f7c

File tree

Expand file treeCollapse file tree

3 files changed

+131
-11
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+131
-11
lines changed
Open diff view settings
Collapse file

‎lib/internal/main/watch_mode.js‎

Copy file name to clipboardExpand all lines: lib/internal/main/watch_mode.js
+21-11Lines changed: 21 additions & 11 deletions
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.
Collapse file

‎lib/internal/watch_mode/files_watcher.js‎

Copy file name to clipboardExpand all lines: lib/internal/watch_mode/files_watcher.js
+29Lines changed: 29 additions & 0 deletions
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
}
Collapse file

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

Copy file name to clipboardExpand all lines: test/sequential/test-watch-mode.mjs
+81Lines 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)
@@ -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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.