diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 4fae6363226310..e2ef4a9cba105a 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -64,6 +64,7 @@ function start() { process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`); } }); + return child; } async function killAndWait(signal = kKillSignal, force = false) { @@ -96,29 +97,31 @@ function reportGracefulTermination() { }; } -async function stop() { +async function stop(child) { + // Without this line, the child process is still able to receive IPC, but is unable to send additional messages + watcher.destroyIPC(child); watcher.clearFileFilters(); const clearGraceReport = reportGracefulTermination(); await killAndWait(); clearGraceReport(); } -async function restart() { +async function restart(child) { if (!kPreserveOutput) process.stdout.write(clear); process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`); - await stop(); - start(); + await stop(child); + return start(); } (async () => { emitExperimentalWarning('Watch mode'); - + let child; try { - start(); + child = start(); // eslint-disable-next-line no-unused-vars for await (const _ of on(watcher, 'changed')) { - await restart(); + child = await restart(child); } } catch (error) { triggerUncaughtException(error, true /* fromPromise */); diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 895c6ec138d131..5785e9ed6b69d3 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -30,6 +30,7 @@ class FilesWatcher extends EventEmitter { #debounce; #mode; #signal; + #wantsPassthroughIPC = false; constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) { super(); @@ -39,6 +40,7 @@ class FilesWatcher extends EventEmitter { this.#debounce = debounce; this.#mode = mode; this.#signal = signal; + this.#wantsPassthroughIPC = !!process.send; if (signal) { EventEmitter.addAbortListener(signal, () => this.clear()); @@ -124,7 +126,28 @@ class FilesWatcher extends EventEmitter { this.#ownerDependencies.set(owner, dependencies); } } + + + #setupIPC(child) { + child._ipcMessages = { + parentToChild: (message) => child.send(message), + childToParent: (message) => process.send(message), + }; + process.on('message', child._ipcMessages.parentToChild); + child.on('message', child._ipcMessages.childToParent); + } + + destroyIPC(child) { + if (this.#wantsPassthroughIPC) { + process.off('message', child._ipcMessages.parentToChild); + child.off('message', child._ipcMessages.childToParent); + } + } + watchChildProcessModules(child, key = null) { + if (this.#wantsPassthroughIPC) { + this.#setupIPC(child); + } if (this.#mode !== 'filter') { return; } diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 444e296bb9f062..ef9b9fb18181dc 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -8,6 +8,7 @@ import { spawn } from 'node:child_process'; import { writeFileSync, readFileSync, mkdirSync } from 'node:fs'; import { inspect } from 'node:util'; import { pathToFileURL } from 'node:url'; +import { once } from 'node:events'; import { createInterface } from 'node:readline'; if (common.isIBMi) @@ -293,6 +294,7 @@ console.log(values.random); ]); }); + // TODO: Remove skip after https://github.com/nodejs/node/pull/45271 lands it('should not watch when running an missing file', { skip: !supportsRecursive @@ -356,4 +358,83 @@ console.log(values.random); `Completed running ${inspect(file)}`, ]); }); + + it('should pass IPC messages from a spawning parent to the child and back', async () => { + const file = createTmpFile(`console.log('running'); +process.on('message', (message) => { + if (message === 'exit') { + process.exit(0); + } else { + console.log('Received:', message); + process.send(message); + } +})`); + + const child = spawn( + execPath, + [ + '--watch', + '--no-warnings', + file, + ], + { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }, + ); + + let stderr = ''; + let stdout = ''; + + child.stdout.on('data', (data) => stdout += data); + child.stderr.on('data', (data) => stderr += data); + async function waitForEcho(msg) { + const receivedPromise = new Promise((resolve) => { + const fn = (message) => { + if (message === msg) { + child.off('message', fn); + resolve(); + } + }; + child.on('message', fn); + }); + child.send(msg); + await receivedPromise; + } + + async function waitForText(text) { + const seenPromise = new Promise((resolve) => { + const fn = (data) => { + if (data.toString().includes(text)) { + resolve(); + child.stdout.off('data', fn); + } + }; + child.stdout.on('data', fn); + }); + await seenPromise; + } + + await waitForEcho('first message'); + const stopRestarts = restart(file); + await waitForText('running'); + stopRestarts(); + await waitForEcho('second message'); + const exitedPromise = once(child, 'exit'); + child.send('exit'); + await waitForText('Completed'); + child.disconnect(); + child.kill(); + await exitedPromise; + assert.strictEqual(stderr, ''); + const lines = stdout.split(/\r?\n/).filter(Boolean); + assert.deepStrictEqual(lines, [ + 'running', + 'Received: first message', + `Restarting '${file}'`, + 'running', + 'Received: second message', + `Completed running ${inspect(file)}`, + ]); + }); });