Skip to content

Commit d2ee81b

Browse files
weiliang79jaysoo
authored andcommitted
feat(core): add shutdown lifecycle hook to node executor (#27354)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> 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 <!-- This is the behavior we should expect with the changes in this PR --> 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) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #9237 and #18037 --------- Co-authored-by: Jack Hsu <[email protected]> (cherry picked from commit b5a9336)
1 parent 25e4bbe commit d2ee81b

File tree

4 files changed

+58
-23
lines changed

4 files changed

+58
-23
lines changed
Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
1-
const Module = require('module');
21
const url = require('node:url');
3-
const originalLoader = Module._load;
2+
const { patchSigint } = require('./patch-sigint');
3+
const { patchRequire } = require('./patch-require');
44

5-
const dynamicImport = new Function('specifier', 'return import(specifier)');
6-
7-
const mappings = JSON.parse(process.env.NX_MAPPINGS);
8-
const keys = Object.keys(mappings);
9-
const fileToRun = url.pathToFileURL(process.env.NX_FILE_TO_RUN);
5+
patchSigint();
6+
patchRequire();
107

11-
Module._load = function (request, parent) {
12-
if (!parent) return originalLoader.apply(this, arguments);
13-
const match = keys.find((k) => request === k);
14-
if (match) {
15-
const newArguments = [...arguments];
16-
newArguments[0] = mappings[match];
17-
return originalLoader.apply(this, newArguments);
18-
} else {
19-
return originalLoader.apply(this, arguments);
20-
}
21-
};
22-
23-
dynamicImport(fileToRun);
8+
const dynamicImport = new Function('specifier', 'return import(specifier)');
9+
dynamicImport(url.pathToFileURL(process.env.NX_FILE_TO_RUN));

packages/js/src/executors/node/node.impl.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,18 @@ export async function* nodeExecutor(
172172
task.childProcess.stderr.on('data', handleStdErr);
173173
task.childProcess.once('exit', (code) => {
174174
task.childProcess.off('data', handleStdErr);
175-
if (options.watch && !task.killed) {
175+
if (
176+
options.watch &&
177+
!task.killed &&
178+
// SIGINT should exist the process rather than watch for changes.
179+
code !== 130
180+
) {
176181
logger.info(
177182
`NX Process exited with code ${code}, waiting for changes to restart...`
178183
);
179184
}
180-
if (!options.watch) {
181-
if (code !== 0) {
185+
if (!options.watch || code === 130) {
186+
if (code !== 0 && code !== 130) {
182187
error(new Error(`Process exited with code ${code}`));
183188
} else {
184189
done();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const Module = require('node:module');
2+
const originalLoader = Module._load;
3+
4+
/**
5+
* Overrides require calls to map buildable workspace libs to their output location.
6+
* This is useful for running programs compiled via TSC/SWC that aren't bundled.
7+
*/
8+
export function patchRequire() {
9+
const mappings = JSON.parse(process.env.NX_MAPPINGS);
10+
const keys = Object.keys(mappings);
11+
12+
Module._load = function (request, parent) {
13+
if (!parent) return originalLoader.apply(this, arguments);
14+
const match = keys.find((k) => request === k);
15+
if (match) {
16+
const newArguments = [...arguments];
17+
newArguments[0] = mappings[match];
18+
return originalLoader.apply(this, newArguments);
19+
} else {
20+
return originalLoader.apply(this, arguments);
21+
}
22+
};
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const readline = require('node:readline');
2+
3+
/**
4+
* Patches the current process so that Ctrl+C is properly handled.
5+
* Without this patch, SIGINT or Ctrl+C does not wait for graceful shutdown and exits immediately.
6+
*/
7+
export function patchSigint() {
8+
readline.emitKeypressEvents(process.stdin);
9+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
10+
process.stdin.on('keypress', async (chunk, key) => {
11+
if (key && key.ctrl && key.name === 'c') {
12+
process.stdin.setRawMode(false); // To ensure nx terminal is not stuck in raw mode
13+
const listeners = process.listeners('SIGINT');
14+
for (const listener of listeners) {
15+
await listener('SIGINT');
16+
}
17+
process.exit(130);
18+
}
19+
});
20+
console.log('To exit the process, press Ctrl+C');
21+
}

0 commit comments

Comments
 (0)