Skip to content

Commit dd83bd2

Browse files
cjihrigMylesBorins
authored andcommitted
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: #32101 Fixes: #32093 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Tobias Nießen <[email protected]>
1 parent 7c739aa commit dd83bd2

File tree

4 files changed

+60
-5
lines changed

4 files changed

+60
-5
lines changed

doc/api/wasi.md

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ added: v13.3.0
5858
sandbox directory structure. The string keys of `preopens` are treated as
5959
directories within the sandbox. The corresponding values in `preopens` are
6060
the real paths to those directories on the host machine.
61+
* `returnOnExit` {boolean} By default, WASI applications terminate the Node.js
62+
process via the `__wasi_proc_exit()` function. Setting this option to `true`
63+
causes `wasi.start()` to return the exit code rather than terminate the
64+
process. **Default:** `false`.
6165

6266
### `wasi.start(instance)`
6367
<!-- YAML

lib/wasi.js

+34-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
} = require('internal/errors').codes;
1616
const { emitExperimentalWarning } = require('internal/util');
1717
const { WASI: _WASI } = internalBinding('wasi');
18+
const kExitCode = Symbol('exitCode');
1819
const kSetMemory = Symbol('setMemory');
1920
const kStarted = Symbol('started');
2021

@@ -26,7 +27,7 @@ class WASI {
2627
if (options === null || typeof options !== 'object')
2728
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
2829

29-
const { env, preopens } = options;
30+
const { env, preopens, returnOnExit = false } = options;
3031
let { args = [] } = options;
3132

3233
if (ArrayIsArray(args))
@@ -56,16 +57,26 @@ class WASI {
5657
throw new ERR_INVALID_ARG_TYPE('options.preopens', 'Object', preopens);
5758
}
5859

60+
if (typeof returnOnExit !== 'boolean') {
61+
throw new ERR_INVALID_ARG_TYPE(
62+
'options.returnOnExit', 'boolean', returnOnExit);
63+
}
64+
5965
const wrap = new _WASI(args, envPairs, preopenArray);
6066

6167
for (const prop in wrap) {
6268
wrap[prop] = FunctionPrototypeBind(wrap[prop], wrap);
6369
}
6470

71+
if (returnOnExit) {
72+
wrap.proc_exit = FunctionPrototypeBind(wasiReturnOnProcExit, this);
73+
}
74+
6575
this[kSetMemory] = wrap._setMemory;
6676
delete wrap._setMemory;
6777
this.wasiImport = wrap;
6878
this[kStarted] = false;
79+
this[kExitCode] = 0;
6980
}
7081

7182
start(instance) {
@@ -93,12 +104,30 @@ class WASI {
93104
this[kStarted] = true;
94105
this[kSetMemory](memory);
95106

96-
if (exports._start)
97-
exports._start();
98-
else if (exports.__wasi_unstable_reactor_start)
99-
exports.__wasi_unstable_reactor_start();
107+
try {
108+
if (exports._start)
109+
exports._start();
110+
else if (exports.__wasi_unstable_reactor_start)
111+
exports.__wasi_unstable_reactor_start();
112+
} catch (err) {
113+
if (err !== kExitCode) {
114+
throw err;
115+
}
116+
}
117+
118+
return this[kExitCode];
100119
}
101120
}
102121

103122

104123
module.exports = { WASI };
124+
125+
126+
function wasiReturnOnProcExit(rval) {
127+
// If __wasi_proc_exit() does not terminate the process, an assertion is
128+
// triggered in the wasm runtime. Node can sidestep the assertion and return
129+
// an exit code by recording the exit code, and throwing a JavaScript
130+
// exception that WebAssembly cannot catch.
131+
this[kExitCode] = rval;
132+
throw kExitCode;
133+
}

test/wasi/test-return-on-exit.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Flags: --experimental-wasi-unstable-preview1 --experimental-wasm-bigint
2+
'use strict';
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const fs = require('fs');
6+
const path = require('path');
7+
const { WASI } = require('wasi');
8+
const wasi = new WASI({ returnOnExit: true });
9+
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
10+
const wasmDir = path.join(__dirname, 'wasm');
11+
const modulePath = path.join(wasmDir, 'exitcode.wasm');
12+
const buffer = fs.readFileSync(modulePath);
13+
14+
(async () => {
15+
const { instance } = await WebAssembly.instantiate(buffer, importObject);
16+
17+
assert.strictEqual(wasi.start(instance), 120);
18+
})().then(common.mustCall());

test/wasi/test-wasi-options-validation.js

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ assert.throws(() => { new WASI({ env: 'fhqwhgads' }); },
2121
assert.throws(() => { new WASI({ preopens: 'fhqwhgads' }); },
2222
{ code: 'ERR_INVALID_ARG_TYPE', message: /\bpreopens\b/ });
2323

24+
// If returnOnExit is not a boolean and not undefined, it should throw.
25+
assert.throws(() => { new WASI({ returnOnExit: 'fhqwhgads' }); },
26+
{ code: 'ERR_INVALID_ARG_TYPE', message: /\breturnOnExit\b/ });
27+
2428
// If options is provided, but not an object, the constructor should throw.
2529
[null, 'foo', '', 0, NaN, Symbol(), true, false, () => {}].forEach((value) => {
2630
assert.throws(() => { new WASI(value); },

0 commit comments

Comments
 (0)