From b5a93364c582df2f73621c9d69cfc5ff29494f64 Mon Sep 17 00:00:00 2001 From: Wei Liang Date: Sat, 17 Aug 2024 02:23:18 +0800 Subject: [PATCH] feat(core): add shutdown lifecycle hook to node executor (#27354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Current Behavior When the application are received a shutdown signal, the application doesn't execute before shutdown functions and directly shutdown whole application. The situation cannot execute before shutdown functions like [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events) and custom shutdown hooks. ## Expected Behavior The application can run shutdown hooks like below output: NX Successfully ran target build for project nest-test (5s) Debugger listening on ws://localhost:9229/e4bd44c0-9a6a-468a-8b46-b6fef1cef1c7 For help, see: https://nodejs.org/en/docs/inspector ``` NX Successfully ran target build for project nest-test (4s) Debugger listening on ws://localhost:9229/75c8449b-43a4-4d8b-88c0-231761d7248c For help, see: https://nodejs.org/en/docs/inspector To exit the process with SIGINT, press Ctrl+C [Nest] 393107 - 08/09/2024, 6:31:26 PM LOG [NestFactory] Starting Nest application... [Nest] 393107 - 08/09/2024, 6:31:26 PM LOG [InstanceLoader] AppModule dependencies initialized +10ms [Nest] 393107 - 08/09/2024, 6:31:26 PM LOG [RoutesResolver] AppController {/api}: +7ms [Nest] 393107 - 08/09/2024, 6:31:26 PM LOG [RouterExplorer] Mapped {/api, GET} route +3ms [Nest] 393107 - 08/09/2024, 6:31:26 PM LOG [NestApplication] Nest application successfully started +2ms [Nest] 393107 - 08/09/2024, 6:31:26 PM LOG 🚀 Application is running on: http://localhost:3000/api [Nest] 393107 - 08/09/2024, 6:31:29 PM LOG onModuleDestroy onModuleDestroy: 5.001s [Nest] 393107 - 08/09/2024, 6:31:34 PM LOG beforeApplicationShutdown SIGINT beforeApplicationShutdown: 5.004s [Nest] 393107 - 08/09/2024, 6:31:39 PM LOG onApplicationShutdown SIGINT onApplicationShutdown: 5.005s NX Process exited with code 130, waiting for changes to restart... ``` ## Related Issue(s) Fixes #9237 and #18037 --------- Co-authored-by: Jack Hsu --- .../node/node-with-require-overrides.ts | 26 +++++-------------- packages/js/src/executors/node/node.impl.ts | 11 +++++--- .../js/src/executors/node/patch-require.ts | 23 ++++++++++++++++ .../js/src/executors/node/patch-sigint.ts | 21 +++++++++++++++ 4 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 packages/js/src/executors/node/patch-require.ts create mode 100644 packages/js/src/executors/node/patch-sigint.ts diff --git a/packages/js/src/executors/node/node-with-require-overrides.ts b/packages/js/src/executors/node/node-with-require-overrides.ts index a67202a792..704a484d47 100644 --- a/packages/js/src/executors/node/node-with-require-overrides.ts +++ b/packages/js/src/executors/node/node-with-require-overrides.ts @@ -1,23 +1,9 @@ -const Module = require('module'); const url = require('node:url'); -const originalLoader = Module._load; +const { patchSigint } = require('./patch-sigint'); +const { patchRequire } = require('./patch-require'); + +patchSigint(); +patchRequire(); const dynamicImport = new Function('specifier', 'return import(specifier)'); - -const mappings = JSON.parse(process.env.NX_MAPPINGS); -const keys = Object.keys(mappings); -const fileToRun = url.pathToFileURL(process.env.NX_FILE_TO_RUN); - -Module._load = function (request, parent) { - if (!parent) return originalLoader.apply(this, arguments); - const match = keys.find((k) => request === k); - if (match) { - const newArguments = [...arguments]; - newArguments[0] = mappings[match]; - return originalLoader.apply(this, newArguments); - } else { - return originalLoader.apply(this, arguments); - } -}; - -dynamicImport(fileToRun); +dynamicImport(url.pathToFileURL(process.env.NX_FILE_TO_RUN)); diff --git a/packages/js/src/executors/node/node.impl.ts b/packages/js/src/executors/node/node.impl.ts index dcbfb85699..fe46a906c3 100644 --- a/packages/js/src/executors/node/node.impl.ts +++ b/packages/js/src/executors/node/node.impl.ts @@ -172,13 +172,18 @@ export async function* nodeExecutor( task.childProcess.stderr.on('data', handleStdErr); task.childProcess.once('exit', (code) => { task.childProcess.off('data', handleStdErr); - if (options.watch && !task.killed) { + if ( + options.watch && + !task.killed && + // SIGINT should exist the process rather than watch for changes. + code !== 130 + ) { logger.info( `NX Process exited with code ${code}, waiting for changes to restart...` ); } - if (!options.watch) { - if (code !== 0) { + if (!options.watch || code === 130) { + if (code !== 0 && code !== 130) { error(new Error(`Process exited with code ${code}`)); } else { done(); diff --git a/packages/js/src/executors/node/patch-require.ts b/packages/js/src/executors/node/patch-require.ts new file mode 100644 index 0000000000..8bec535651 --- /dev/null +++ b/packages/js/src/executors/node/patch-require.ts @@ -0,0 +1,23 @@ +const Module = require('node:module'); +const originalLoader = Module._load; + +/** + * Overrides require calls to map buildable workspace libs to their output location. + * This is useful for running programs compiled via TSC/SWC that aren't bundled. + */ +export function patchRequire() { + const mappings = JSON.parse(process.env.NX_MAPPINGS); + const keys = Object.keys(mappings); + + Module._load = function (request, parent) { + if (!parent) return originalLoader.apply(this, arguments); + const match = keys.find((k) => request === k); + if (match) { + const newArguments = [...arguments]; + newArguments[0] = mappings[match]; + return originalLoader.apply(this, newArguments); + } else { + return originalLoader.apply(this, arguments); + } + }; +} diff --git a/packages/js/src/executors/node/patch-sigint.ts b/packages/js/src/executors/node/patch-sigint.ts new file mode 100644 index 0000000000..286887524d --- /dev/null +++ b/packages/js/src/executors/node/patch-sigint.ts @@ -0,0 +1,21 @@ +const readline = require('node:readline'); + +/** + * Patches the current process so that Ctrl+C is properly handled. + * Without this patch, SIGINT or Ctrl+C does not wait for graceful shutdown and exits immediately. + */ +export function patchSigint() { + readline.emitKeypressEvents(process.stdin); + if (process.stdin.isTTY) process.stdin.setRawMode(true); + process.stdin.on('keypress', async (chunk, key) => { + if (key && key.ctrl && key.name === 'c') { + process.stdin.setRawMode(false); // To ensure nx terminal is not stuck in raw mode + const listeners = process.listeners('SIGINT'); + for (const listener of listeners) { + await listener('SIGINT'); + } + process.exit(130); + } + }); + console.log('To exit the process, press Ctrl+C'); +}