Skip to content

Commit

Permalink
wasi: add returnOnExit option
Browse files Browse the repository at this point in the history
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: nodejs#32101
Fixes: nodejs#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>
  • Loading branch information
cjihrig authored and targos committed Apr 25, 2020
1 parent 372d30c commit 0ab1ebd
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 5 deletions.
4 changes: 4 additions & 0 deletions doc/api/wasi.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ added: v12.16.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
Expand Down
39 changes: 34 additions & 5 deletions lib/wasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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');

Expand All @@ -25,7 +26,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))
Expand Down Expand Up @@ -55,16 +56,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) {
Expand Down Expand Up @@ -92,12 +103,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;
}
18 changes: 18 additions & 0 deletions test/wasi/test-return-on-exit.js
Original file line number Diff line number Diff line change
@@ -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());
4 changes: 4 additions & 0 deletions test/wasi/test-wasi-options-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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); },
Expand Down

0 comments on commit 0ab1ebd

Please sign in to comment.