diff --git a/doc/api/process.md b/doc/api/process.md index 7fc8a736a36011..874712fb612d19 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1893,8 +1893,28 @@ A number which will be the process exit code, when the process either exits gracefully, or is exited via [`process.exit()`][] without specifying a code. -Specifying a code to [`process.exit(code)`][`process.exit()`] will override any -previous setting of `process.exitCode`. +The value of `process.exitCode` can be updated by either assigning a value to +`process.exitCode` or by passing an argument to [`process.exit()`][]: + +```console +$ node -e 'process.exitCode = 9'; echo $? +9 +$ node -e 'process.exit(42)'; echo $? +42 +$ node -e 'process.exitCode = 9; process.exit(42)'; echo $? +42 +``` + +The value can also be set implicitly by Node.js when unrecoverable errors occur (e.g. +such as the encountering of an unsettled top-level await). However explicit +manipulations of the exit code always take precedence over implicit ones: + +```console +$ node --input-type=module -e 'await new Promise(() => {})'; echo $? +13 +$ node --input-type=module -e 'process.exitCode = 9; await new Promise(() => {})'; echo $? +9 +``` ## `process.features.cached_builtins` diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index e72e2185b54998..003e67565571d3 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -104,11 +104,10 @@ process.domain = null; configurable: true, }); - let exitCode; ObjectDefineProperty(process, 'exitCode', { __proto__: null, get() { - return exitCode; + return fields[kHasExitCode] ? fields[kExitCode] : undefined; }, set(code) { if (code !== null && code !== undefined) { @@ -123,7 +122,6 @@ process.domain = null; } else { fields[kHasExitCode] = 0; } - exitCode = code; }, enumerable: true, configurable: false, diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index 34de89a8dc0398..14d2a20c1c18d9 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -73,20 +73,7 @@ Maybe SpinEventLoopInternal(Environment* env) { env->PrintInfoForSnapshotIfDebug(); env->ForEachRealm([](Realm* realm) { realm->VerifyNoStrongBaseObjects(); }); - Maybe exit_code = EmitProcessExitInternal(env); - if (exit_code.FromMaybe(ExitCode::kGenericUserError) != - ExitCode::kNoFailure) { - return exit_code; - } - - auto unsettled_tla = env->CheckUnsettledTopLevelAwait(); - if (unsettled_tla.IsNothing()) { - return Nothing(); - } - if (!unsettled_tla.FromJust()) { - return Just(ExitCode::kUnsettledTopLevelAwait); - } - return Just(ExitCode::kNoFailure); + return EmitProcessExitInternal(env); } struct CommonEnvironmentSetup::Impl { diff --git a/src/api/hooks.cc b/src/api/hooks.cc index cf95d009d4a088..54163a59f2f340 100644 --- a/src/api/hooks.cc +++ b/src/api/hooks.cc @@ -70,14 +70,26 @@ Maybe EmitProcessExitInternal(Environment* env) { return Nothing(); } - Local exit_code = Integer::New( - isolate, static_cast(env->exit_code(ExitCode::kNoFailure))); + ExitCode exit_code = env->exit_code(ExitCode::kNoFailure); + + // the exit code wasn't already set, so let's check for unsettled tlas + if (exit_code == ExitCode::kNoFailure) { + auto unsettled_tla = env->CheckUnsettledTopLevelAwait(); + if (!unsettled_tla.FromJust()) { + exit_code = ExitCode::kUnsettledTopLevelAwait; + env->set_exit_code(exit_code); + } + } - if (ProcessEmit(env, "exit", exit_code).IsEmpty()) { + Local exit_code_int = + Integer::New(isolate, static_cast(exit_code)); + + if (ProcessEmit(env, "exit", exit_code_int).IsEmpty()) { return Nothing(); } + // Reload exit code, it may be changed by `emit('exit')` - return Just(env->exit_code(ExitCode::kNoFailure)); + return Just(env->exit_code(exit_code)); } Maybe EmitProcessExit(Environment* env) { diff --git a/src/env-inl.h b/src/env-inl.h index 23bc4f47ad8044..a9945c1c41e1ed 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -341,6 +341,11 @@ inline ExitCode Environment::exit_code(const ExitCode default_code) const { : static_cast(exit_info_[kExitCode]); } +inline void Environment::set_exit_code(const ExitCode code) { + exit_info_[kExitCode] = static_cast(code); + exit_info_[kHasExitCode] = 1; +} + inline AliasedInt32Array& Environment::exit_info() { return exit_info_; } diff --git a/src/env.h b/src/env.h index de0bf985ff4ab7..2f82bbba3e03e4 100644 --- a/src/env.h +++ b/src/env.h @@ -739,6 +739,8 @@ class Environment final : public MemoryRetainer { bool exiting() const; inline ExitCode exit_code(const ExitCode default_code) const; + inline void set_exit_code(const ExitCode code); + // This stores whether the --abort-on-uncaught-exception flag was passed // to Node. inline bool abort_on_uncaught_exception() const; diff --git a/test/es-module/test-esm-tla-unfinished.mjs b/test/es-module/test-esm-tla-unfinished.mjs index 4c6e0708daaf08..ee6bc3b5fa2579 100644 --- a/test/es-module/test-esm-tla-unfinished.mjs +++ b/test/es-module/test-esm-tla-unfinished.mjs @@ -76,9 +76,9 @@ describe('ESM: unsettled and rejected promises', { concurrency: !process.env.TES fixtures.path('es-modules/tla/unresolved.mjs'), ]); - assert.match(stderr, /Warning: Detected unsettled top-level await at.+unresolved\.mjs:1/); + assert.match(stderr, /Warning: Detected unsettled top-level await at.+unresolved\.mjs:5\b/); assert.match(stderr, /await new Promise/); - assert.strictEqual(stdout, ''); + assert.strictEqual(stdout, 'the exit listener received code: 13\n'); assert.strictEqual(code, 13); }); @@ -88,9 +88,11 @@ describe('ESM: unsettled and rejected promises', { concurrency: !process.env.TES fixtures.path('es-modules/tla/unresolved.mjs'), ]); - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 13); + assert.deepStrictEqual({ code, stdout, stderr }, { + code: 13, + stdout: 'the exit listener received code: 13\n', + stderr: '', + }); }); it('should throw for a rejected TLA promise via stdin', async () => { @@ -104,15 +106,17 @@ describe('ESM: unsettled and rejected promises', { concurrency: !process.env.TES assert.strictEqual(code, 1); }); - it('should exit for an unsettled TLA promise and respect explicit exit code via stdin', async () => { + it('should exit for an unsettled TLA promise and respect explicit exit code', async () => { const { code, stderr, stdout } = await spawnPromisified(execPath, [ '--no-warnings', fixtures.path('es-modules/tla/unresolved-withexitcode.mjs'), ]); - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 42); + assert.deepStrictEqual({ code, stdout, stderr }, { + code: 42, + stdout: 'the exit listener received code: 42\n', + stderr: '', + }); }); it('should throw for a rejected TLA promise and ignore explicit exit code via stdin', async () => { @@ -158,4 +162,33 @@ describe('ESM: unsettled and rejected promises', { concurrency: !process.env.TES assert.strictEqual(stdout, ''); assert.strictEqual(code, 13); }); + + describe('with exit listener', () => { + it('the process exit event should provide the correct code', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + fixtures.path('es-modules/tla/unresolved-with-listener.mjs'), + ]); + + assert.match(stderr, /Warning: Detected unsettled top-level await at/); + assert.strictEqual(stdout, + 'the exit listener received code: 13\n' + + 'process.exitCode inside the exist listener: 13\n' + ); + assert.strictEqual(code, 13); + }); + + it('should exit for an unsettled TLA promise and respect explicit exit code in process exit event', async () => { + const { code, stderr, stdout } = await spawnPromisified(execPath, [ + '--no-warnings', + fixtures.path('es-modules/tla/unresolved-withexitcode-and-listener.mjs'), + ]); + + assert.deepStrictEqual({ code, stdout, stderr }, { + code: 42, + stdout: 'the exit listener received code: 42\n' + + 'process.exitCode inside the exist listener: 42\n', + stderr: '', + }); + }); + }); }); diff --git a/test/fixtures/es-modules/tla/unresolved-with-listener.mjs b/test/fixtures/es-modules/tla/unresolved-with-listener.mjs new file mode 100644 index 00000000000000..8bd2c0a080503c --- /dev/null +++ b/test/fixtures/es-modules/tla/unresolved-with-listener.mjs @@ -0,0 +1,6 @@ +process.on('exit', (exitCode) => { + console.log(`the exit listener received code: ${exitCode}`); + console.log(`process.exitCode inside the exist listener: ${process.exitCode}`); +}) + +await new Promise(() => {}); diff --git a/test/fixtures/es-modules/tla/unresolved-withexitcode-and-listener.mjs b/test/fixtures/es-modules/tla/unresolved-withexitcode-and-listener.mjs new file mode 100644 index 00000000000000..fa18609123e233 --- /dev/null +++ b/test/fixtures/es-modules/tla/unresolved-withexitcode-and-listener.mjs @@ -0,0 +1,8 @@ +process.on('exit', (exitCode) => { + console.log(`the exit listener received code: ${exitCode}`); + console.log(`process.exitCode inside the exist listener: ${process.exitCode}`); +}); + +process.exitCode = 42; + +await new Promise(() => {}); diff --git a/test/fixtures/es-modules/tla/unresolved-withexitcode.mjs b/test/fixtures/es-modules/tla/unresolved-withexitcode.mjs index 1cb982311080b8..0316dae1cd9a3c 100644 --- a/test/fixtures/es-modules/tla/unresolved-withexitcode.mjs +++ b/test/fixtures/es-modules/tla/unresolved-withexitcode.mjs @@ -1,2 +1,7 @@ +process.on('exit', (exitCode) => { + console.log(`the exit listener received code: ${exitCode}`); +}); + process.exitCode = 42; + await new Promise(() => {}); diff --git a/test/fixtures/es-modules/tla/unresolved.mjs b/test/fixtures/es-modules/tla/unresolved.mjs index 231a8cd634825c..37566bd5688fb5 100644 --- a/test/fixtures/es-modules/tla/unresolved.mjs +++ b/test/fixtures/es-modules/tla/unresolved.mjs @@ -1 +1,5 @@ +process.on('exit', (exitCode) => { + console.log(`the exit listener received code: ${exitCode}`); +}) + await new Promise(() => {});