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: #32101Fixes: #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‎

Lines changed: 4 additions & 0 deletions
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‎

Lines changed: 34 additions & 5 deletions
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+
constkExitCode=Symbol('exitCode');
1819
constkSetMemory=Symbol('setMemory');
1920
constkStarted=Symbol('started');
2021

@@ -26,7 +27,7 @@ class WASI{
2627
if(options===null||typeofoptions!=='object')
2728
thrownewERR_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
thrownewERR_INVALID_ARG_TYPE('options.preopens','Object',preopens);
5758
}
5859

60+
if(typeofreturnOnExit!=='boolean'){
61+
thrownewERR_INVALID_ARG_TYPE(
62+
'options.returnOnExit','boolean',returnOnExit);
63+
}
64+
5965
constwrap=new_WASI(args,envPairs,preopenArray);
6066

6167
for(constpropinwrap){
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
deletewrap._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-
elseif(exports.__wasi_unstable_reactor_start)
99-
exports.__wasi_unstable_reactor_start();
107+
try{
108+
if(exports._start)
109+
exports._start();
110+
elseif(exports.__wasi_unstable_reactor_start)
111+
exports.__wasi_unstable_reactor_start();
112+
}catch(err){
113+
if(err!==kExitCode){
114+
throwerr;
115+
}
116+
}
117+
118+
returnthis[kExitCode];
100119
}
101120
}
102121

103122

104123
module.exports={WASI};
124+
125+
126+
functionwasiReturnOnProcExit(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+
throwkExitCode;
133+
}

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

Lines changed: 18 additions & 0 deletions
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+
constcommon=require('../common');
4+
constassert=require('assert');
5+
constfs=require('fs');
6+
constpath=require('path');
7+
const{WASI}=require('wasi');
8+
constwasi=newWASI({returnOnExit: true});
9+
constimportObject={wasi_snapshot_preview1: wasi.wasiImport};
10+
constwasmDir=path.join(__dirname,'wasm');
11+
constmodulePath=path.join(wasmDir,'exitcode.wasm');
12+
constbuffer=fs.readFileSync(modulePath);
13+
14+
(async()=>{
15+
const{ instance }=awaitWebAssembly.instantiate(buffer,importObject);
16+
17+
assert.strictEqual(wasi.start(instance),120);
18+
})().then(common.mustCall());

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ assert.throws(() =>{new WASI({env: 'fhqwhgads'})},
2121
assert.throws(()=>{newWASI({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(()=>{newWASI({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(()=>{newWASI(value);},

0 commit comments

Comments
(0)