module: handle Top-Level Await non-fulfills better

Handle situations in which the main `Promise` from a TLA module
is not fulfilled better:

- When not resolving the `Promise` at all, set a non-zero exit code
  (unless another one has been requested explicitly) to distinguish
  the result from a successful completion.
- When rejecting the `Promise`, always treat it like an uncaught
  exception. In particular, this also ensures a non-zero exit code.

Refs: https://github.com/nodejs/node/pull/34558

PR-URL: https://github.com/nodejs/node/pull/34640
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Yongsheng Zhang <zyszys98@gmail.com>
pull/34643/head
Anna Henningsen 2020-08-06 02:56:17 +02:00
parent f32114441c
commit e948ef351b
No known key found for this signature in database
GPG Key ID: A94130F0BFC8EBE9
8 changed files with 111 additions and 14 deletions

View File

@ -2586,6 +2586,8 @@ cases:
and generally can only happen during development of Node.js itself.
* `12` **Invalid Debug Argument**: The `--inspect` and/or `--inspect-brk`
options were set, but the port number chosen was invalid or unavailable.
* `13` **Unfinished Top-Level Await**: `await` was used outside of a function
in the top-level code, but the passed `Promise` never resolved.
* `>128` **Signal Exits**: If Node.js receives a fatal signal such as
`SIGKILL` or `SIGHUP`, then its exit code will be `128` plus the
value of the signal code. This is a standard POSIX practice, since

View File

@ -40,11 +40,23 @@ function shouldUseESMLoader(mainPath) {
function runMainESM(mainPath) {
const esmLoader = require('internal/process/esm_loader');
const { pathToFileURL } = require('internal/url');
esmLoader.loadESM((ESMLoader) => {
handleMainPromise(esmLoader.loadESM((ESMLoader) => {
const main = path.isAbsolute(mainPath) ?
pathToFileURL(mainPath).href : mainPath;
return ESMLoader.import(main);
});
}));
}
function handleMainPromise(promise) {
// Handle a Promise from running code that potentially does Top-Level Await.
// In that case, it makes sense to set the exit code to a specific non-zero
// value if the main code never finishes running.
function handler() {
if (process.exitCode === undefined)
process.exitCode = 13;
}
process.on('exit', handler);
return promise.finally(() => process.off('exit', handler));
}
// For backwards compatibility, we have to run a bunch of
@ -62,5 +74,6 @@ function executeUserEntryPoint(main = process.argv[1]) {
}
module.exports = {
executeUserEntryPoint
executeUserEntryPoint,
handleMainPromise,
};

View File

@ -2,7 +2,6 @@
const {
JSONStringify,
PromiseResolve,
} = primordials;
const path = require('path');
@ -43,20 +42,15 @@ function evalModule(source, print) {
if (print) {
throw new ERR_EVAL_ESM_CANNOT_PRINT();
}
const { log, error } = require('internal/console/global');
const { decorateErrorStack } = require('internal/util');
const asyncESM = require('internal/process/esm_loader');
PromiseResolve(asyncESM.ESMLoader).then(async (loader) => {
const { log } = require('internal/console/global');
const { loadESM } = require('internal/process/esm_loader');
const { handleMainPromise } = require('internal/modules/run_main');
handleMainPromise(loadESM(async (loader) => {
const { result } = await loader.eval(source);
if (print) {
log(result);
}
})
.catch((e) => {
decorateErrorStack(e);
error(e);
process.exit(1);
});
}));
}
function evalScript(name, body, breakFirstLine, print) {

View File

@ -0,0 +1,82 @@
import '../common/index.mjs';
import assert from 'assert';
import child_process from 'child_process';
import fixtures from '../common/fixtures.js';
{
// Unresolved TLA promise, --eval
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
['--input-type=module', '--eval', 'await new Promise(() => {})'],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout, stderr], [13, '', '']);
}
{
// Rejected TLA promise, --eval
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
['--input-type=module', '-e', 'await Promise.reject(new Error("Xyz"))'],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout], [1, '']);
assert.match(stderr, /Error: Xyz/);
}
{
// Unresolved TLA promise with explicit exit code, --eval
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
['--input-type=module', '--eval',
'process.exitCode = 42;await new Promise(() => {})'],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout, stderr], [42, '', '']);
}
{
// Rejected TLA promise with explicit exit code, --eval
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
['--input-type=module', '-e',
'process.exitCode = 42;await Promise.reject(new Error("Xyz"))'],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout], [1, '']);
assert.match(stderr, /Error: Xyz/);
}
{
// Unresolved TLA promise, module file
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
[fixtures.path('es-modules/tla/unresolved.mjs')],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout, stderr], [13, '', '']);
}
{
// Rejected TLA promise, module file
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
[fixtures.path('es-modules/tla/rejected.mjs')],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout], [1, '']);
assert.match(stderr, /Error: Xyz/);
}
{
// Unresolved TLA promise, module file
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
[fixtures.path('es-modules/tla/unresolved-withexitcode.mjs')],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout, stderr], [42, '', '']);
}
{
// Rejected TLA promise, module file
const { status, stdout, stderr } = child_process.spawnSync(
process.execPath,
[fixtures.path('es-modules/tla/rejected-withexitcode.mjs')],
{ encoding: 'utf8' });
assert.deepStrictEqual([status, stdout], [1, '']);
assert.match(stderr, /Error: Xyz/);
}

View File

@ -0,0 +1,2 @@
process.exitCode = 42;
await Promise.reject(new Error('Xyz'));

View File

@ -0,0 +1 @@
await Promise.reject(new Error('Xyz'));

View File

@ -0,0 +1,2 @@
process.exitCode = 42;
await new Promise(() => {});

View File

@ -0,0 +1 @@
await new Promise(() => {});