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 <jack.hsu@gmail.com>
This commit is contained in:
Wei Liang 2024-08-17 02:23:18 +08:00 committed by GitHub
parent 958f188bd0
commit b5a93364c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 58 additions and 23 deletions

View File

@ -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));

View File

@ -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();

View File

@ -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);
}
};
}

View File

@ -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');
}