wasi: add returnOnExit option

This commit adds a WASI option allowing the __wasi_proc_exit()
function to return an exit code instead of forcefully terminating
the process.

PR-URL: https://github.com/nodejs/node/pull/32101
Fixes: https://github.com/nodejs/node/issues/32093
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
cjihrig 2020-03-04 20:46:02 -05:00
parent 4d93e105bf
commit 6aff62fcb3
No known key found for this signature in database
GPG Key ID: 7434390BDBE9B9C5
4 changed files with 60 additions and 5 deletions

View File

@ -58,6 +58,10 @@ added: v13.3.0
sandbox directory structure. The string keys of `preopens` are treated as
directories within the sandbox. The corresponding values in `preopens` are
the real paths to those directories on the host machine.
* `returnOnExit` {boolean} By default, WASI applications terminate the Node.js
process via the `__wasi_proc_exit()` function. Setting this option to `true`
causes `wasi.start()` to return the exit code rather than terminate the
process. **Default:** `false`.
### `wasi.start(instance)`
<!-- YAML

View File

@ -15,6 +15,7 @@ const {
} = require('internal/errors').codes;
const { emitExperimentalWarning } = require('internal/util');
const { WASI: _WASI } = internalBinding('wasi');
const kExitCode = Symbol('exitCode');
const kSetMemory = Symbol('setMemory');
const kStarted = Symbol('started');
@ -26,7 +27,7 @@ class WASI {
if (options === null || typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
const { env, preopens } = options;
const { env, preopens, returnOnExit = false } = options;
let { args = [] } = options;
if (ArrayIsArray(args))
@ -56,16 +57,26 @@ class WASI {
throw new ERR_INVALID_ARG_TYPE('options.preopens', 'Object', preopens);
}
if (typeof returnOnExit !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE(
'options.returnOnExit', 'boolean', returnOnExit);
}
const wrap = new _WASI(args, envPairs, preopenArray);
for (const prop in wrap) {
wrap[prop] = FunctionPrototypeBind(wrap[prop], wrap);
}
if (returnOnExit) {
wrap.proc_exit = FunctionPrototypeBind(wasiReturnOnProcExit, this);
}
this[kSetMemory] = wrap._setMemory;
delete wrap._setMemory;
this.wasiImport = wrap;
this[kStarted] = false;
this[kExitCode] = 0;
}
start(instance) {
@ -93,12 +104,30 @@ class WASI {
this[kStarted] = true;
this[kSetMemory](memory);
if (exports._start)
exports._start();
else if (exports.__wasi_unstable_reactor_start)
exports.__wasi_unstable_reactor_start();
try {
if (exports._start)
exports._start();
else if (exports.__wasi_unstable_reactor_start)
exports.__wasi_unstable_reactor_start();
} catch (err) {
if (err !== kExitCode) {
throw err;
}
}
return this[kExitCode];
}
}
module.exports = { WASI };
function wasiReturnOnProcExit(rval) {
// If __wasi_proc_exit() does not terminate the process, an assertion is
// triggered in the wasm runtime. Node can sidestep the assertion and return
// an exit code by recording the exit code, and throwing a JavaScript
// exception that WebAssembly cannot catch.
this[kExitCode] = rval;
throw kExitCode;
}

View File

@ -0,0 +1,18 @@
// Flags: --experimental-wasi-unstable-preview1 --experimental-wasm-bigint
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { WASI } = require('wasi');
const wasi = new WASI({ returnOnExit: true });
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
const wasmDir = path.join(__dirname, 'wasm');
const modulePath = path.join(wasmDir, 'exitcode.wasm');
const buffer = fs.readFileSync(modulePath);
(async () => {
const { instance } = await WebAssembly.instantiate(buffer, importObject);
assert.strictEqual(wasi.start(instance), 120);
})().then(common.mustCall());

View File

@ -21,6 +21,10 @@ assert.throws(() => { new WASI({ env: 'fhqwhgads' }); },
assert.throws(() => { new WASI({ preopens: 'fhqwhgads' }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\bpreopens\b/ });
// If returnOnExit is not a boolean and not undefined, it should throw.
assert.throws(() => { new WASI({ returnOnExit: 'fhqwhgads' }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\breturnOnExit\b/ });
// If options is provided, but not an object, the constructor should throw.
[null, 'foo', '', 0, NaN, Symbol(), true, false, () => {}].forEach((value) => {
assert.throws(() => { new WASI(value); },