GeezerVison is showing github.com — watch: enable passthrough ipc in watch mode · nodejs/node@e895f7c
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/watch_mode.js +21-11Lines changed: 21 additions & 11 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -81,6 +81,7 @@ function start() { | 81 | 81 | process.stdout.write(`${red}Failed running ${kCommandStr}${white}\n`); | 82 | 82 | } | 83 | 83 | }); | | 84 | + return child; | 84 | 85 | } | 85 | 86 |
| 86 | 87 | async function killAndWait(signal = kKillSignal, force = false) { | @@ -113,34 +114,43 @@ function reportGracefulTermination() { | 113 | 114 | }; | 114 | 115 | } | 115 | 116 |
| 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); | 117 | 120 | watcher.clearFileFilters(); | 118 | 121 | const clearGraceReport = reportGracefulTermination(); | 119 | 122 | await killAndWait(); | 120 | 123 | clearGraceReport(); | 121 | 124 | } | 122 | 125 |
| 123 | 126 | let restarting = false; | 124 | | -async function restart() { | | 127 | +async function restart(child) { | 125 | 128 | if (restarting) return; | 126 | 129 | restarting = true; | 127 | 130 | try { | 128 | 131 | if (!kPreserveOutput) process.stdout.write(clear); | 129 | 132 | process.stdout.write(`${green}Restarting ${kCommandStr}${white}\n`); | 130 | | - await stop(); | 131 | | - start(); | | 133 | + await stop(child); | | 134 | + return start(); | 132 | 135 | } finally { | 133 | 136 | restarting = false; | 134 | 137 | } | 135 | 138 | } | 136 | 139 |
| 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(); | 144 | 154 |
| 145 | 155 | // Exiting gracefully to avoid stdout/stderr getting written after | 146 | 156 | // parent process is killed. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/watch_mode/files_watcher.js +29Lines changed: 29 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | const { | 4 | 4 | ArrayIsArray, | 5 | 5 | ArrayPrototypeForEach, | | 6 | + Boolean, | 6 | 7 | SafeMap, | 7 | 8 | SafeSet, | | 9 | + SafeWeakMap, | 8 | 10 | StringPrototypeStartsWith, | 9 | 11 | } = primordials; | 10 | 12 |
| @@ -31,6 +33,8 @@ class FilesWatcher extends EventEmitter { | 31 | 33 | #debounce; | 32 | 34 | #mode; | 33 | 35 | #signal; | | 36 | + #passthroughIPC = false; | | 37 | + #ipcHandlers = new SafeWeakMap(); | 34 | 38 |
| 35 | 39 | constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) { | 36 | 40 | super({ __proto__: null, captureRejections: true }); | @@ -40,6 +44,7 @@ class FilesWatcher extends EventEmitter { | 40 | 44 | this.#debounce = debounce; | 41 | 45 | this.#mode = mode; | 42 | 46 | this.#signal = signal; | | 47 | + this.#passthroughIPC = Boolean(process.send); | 43 | 48 |
| 44 | 49 | if (signal) { | 45 | 50 | addAbortListener(signal, () => this.clear()); | @@ -128,7 +133,31 @@ class FilesWatcher extends EventEmitter { | 128 | 133 | this.#ownerDependencies.set(owner, dependencies); | 129 | 134 | } | 130 | 135 | } | | 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 | + | 131 | 157 | watchChildProcessModules(child, key = null) { | | 158 | + if (this.#passthroughIPC) { | | 159 | + this.#setupIPC(child); | | 160 | + } | 132 | 161 | if (this.#mode !== 'filter') { | 133 | 162 | return; | 134 | 163 | } | |
Collapse file Copy file name to clipboardExpand all lines: test/sequential/test-watch-mode.mjs +81Lines changed: 81 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,6 +8,7 @@ import { spawn } from 'node:child_process'; | 8 | 8 | import { writeFileSync, readFileSync, mkdirSync } from 'node:fs'; | 9 | 9 | import { inspect } from 'node:util'; | 10 | 10 | import { pathToFileURL } from 'node:url'; | | 11 | +import { once } from 'node:events'; | 11 | 12 | import { createInterface } from 'node:readline'; | 12 | 13 |
| 13 | 14 | if (common.isIBMi) | @@ -574,4 +575,84 @@ console.log(values.random); | 574 | 575 | `Completed running ${inspect(file)}`, | 575 | 576 | ]); | 576 | 577 | }); | | 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 | + }); | 577 | 658 | }); |
|