@napi-rs/cli 3.10.5 → 3.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +772 -103
- package/dist/index.cjs +772 -103
- package/dist/index.d.cts +1 -1
- package/dist/index.js +772 -103
- package/docs/wasi.md +202 -0
- package/package.json +2 -2
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +177 -0
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/templates.spec.ts +1928 -6
- package/src/api/templates/load-wasi-template.ts +606 -7
- package/src/api/templates/wasi-worker-template.ts +191 -63
|
@@ -7,6 +7,47 @@ import {
|
|
|
7
7
|
const WASI_DISPOSE_SYMBOL = 'napi.rs.wasi.dispose'
|
|
8
8
|
const WASI_ROLLBACK_REGISTRY_SYMBOL = 'napi.rs.wasi.rollback.registry.v1'
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Threaded loaders only: a view of the addon's crash flag for the pool workers.
|
|
12
|
+
*
|
|
13
|
+
* napi-async-runtime's shutdown waits run on the loader thread, inside the
|
|
14
|
+
* environment cleanup calls, and check one word of the shared wasm memory
|
|
15
|
+
* between 1 ms slices; once it is set they trap instead of waiting on a dead
|
|
16
|
+
* thread for good. napi's `napi_wasm_thread_crash_flag_address` export says
|
|
17
|
+
* where that word is. Each worker gets an `Int32Array` over it and raises it
|
|
18
|
+
* with `Atomics.store` when its wasm thread dies — right after the loader's own
|
|
19
|
+
* crash flag, where there is one. The store needs no instance, so a worker that
|
|
20
|
+
* fails while it loads, after the thread spawn that created it had already
|
|
21
|
+
* returned, raises it too.
|
|
22
|
+
*
|
|
23
|
+
* Read in `beforeInit`: before any registration code runs, and on Node before
|
|
24
|
+
* any pool worker exists (the pool is created on demand). An addon built with
|
|
25
|
+
* an older napi has no export, and a value that is not a 4-aligned address
|
|
26
|
+
* inside the memory is ignored: the view stays undefined and the waits stay
|
|
27
|
+
* unbounded, as before.
|
|
28
|
+
*/
|
|
29
|
+
const ADDON_CRASH_FLAG_CAPTURE = `// A view of the addon's crash flag, one word of the shared wasm memory, for the
|
|
30
|
+
// pool workers: they raise it when their wasm thread dies, and the shutdown
|
|
31
|
+
// waits inside the cleanup calls on this thread then trap instead of waiting on
|
|
32
|
+
// the dead thread for good. Undefined for an addon built with an older napi.
|
|
33
|
+
let __wasiAddonCrashFlag
|
|
34
|
+
|
|
35
|
+
function __captureWasiAddonCrashFlag(instance) {
|
|
36
|
+
try {
|
|
37
|
+
const getAddress = instance.exports.napi_wasm_thread_crash_flag_address
|
|
38
|
+
if (typeof getAddress !== 'function') {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const address = getAddress() >>> 0
|
|
42
|
+
const buffer = __sharedMemory.buffer
|
|
43
|
+
if (address === 0 || address % 4 !== 0 || address + 4 > buffer.byteLength) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
__wasiAddonCrashFlag = new Int32Array(buffer, address, 1)
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
|
|
10
51
|
/**
|
|
11
52
|
* `Context.destroy()` disables JavaScript calls *before* it runs cleanup hooks
|
|
12
53
|
* (`setStopping` -> `setCanCallIntoJs(false)` -> `runCleanup`), and the
|
|
@@ -88,7 +129,12 @@ function __wrapEmnapiContextDestroyForSettlement(
|
|
|
88
129
|
* `__startWasiDisposal` / the rollback have already prepared and drained, so
|
|
89
130
|
* one call there covers all three.
|
|
90
131
|
*/
|
|
91
|
-
const createEmnapiContextLifecycle = (
|
|
132
|
+
const createEmnapiContextLifecycle = (
|
|
133
|
+
asyncRuntime: boolean,
|
|
134
|
+
// Only the threaded Node loader defines the crash latch
|
|
135
|
+
// (`__hasWasiThreadCrashed`, `__disposeWasiBindingAfterThreadCrash`).
|
|
136
|
+
threadCrashLatch = false,
|
|
137
|
+
) => {
|
|
92
138
|
const currentThreadHosts = asyncRuntime
|
|
93
139
|
? `
|
|
94
140
|
let __currentThreadHostsDisposer
|
|
@@ -122,9 +168,203 @@ function __disposeCurrentThreadHosts() {
|
|
|
122
168
|
}
|
|
123
169
|
`
|
|
124
170
|
: ''
|
|
171
|
+
const currentThreadHostTimersAfterThreadCrash =
|
|
172
|
+
asyncRuntime && threadCrashLatch
|
|
173
|
+
? `
|
|
174
|
+
/**
|
|
175
|
+
* The CurrentThread timer host arms one referenced \`setTimeout\` per sleep in
|
|
176
|
+
* flight, so a long sleep holds the event loop open until it fires.
|
|
177
|
+
* \`__disposeCurrentThreadHosts\` releases them, but only
|
|
178
|
+
* \`__destroyEmnapiContext\` calls it, and the crash disposal never gets there:
|
|
179
|
+
* after a wasm thread died it must not enter wasm, and both the host
|
|
180
|
+
* unregister calls and a settled timer promise do. A dispose() that rejected
|
|
181
|
+
* after a crash then left the process alive until the longest sleep ended.
|
|
182
|
+
*
|
|
183
|
+
* So the loader keeps the package's own \`cancel\` for every timer in flight,
|
|
184
|
+
* and \`__releaseCurrentThreadHostTimers\` calls them after a crash. \`cancel\`
|
|
185
|
+
* is plain JavaScript: it clears the host timeout and resolves the package's
|
|
186
|
+
* promise. The promise the addon holds is the one below, and once released it
|
|
187
|
+
* never settles, so nothing is handed back to wasm. A sleep armed after that
|
|
188
|
+
* is never armed at all.
|
|
189
|
+
*/
|
|
190
|
+
const __currentThreadHostTimerCancels = new Set()
|
|
191
|
+
let __currentThreadHostTimersReleased = false
|
|
192
|
+
|
|
193
|
+
function __scheduleCurrentThreadHostTimer(schedule, cancel, id, ms) {
|
|
194
|
+
if (__currentThreadHostTimersReleased) {
|
|
195
|
+
return new Promise(() => {})
|
|
196
|
+
}
|
|
197
|
+
const scheduled = schedule(id, ms)
|
|
198
|
+
if (!__isThenable(scheduled)) {
|
|
199
|
+
return scheduled
|
|
200
|
+
}
|
|
201
|
+
const release = () => cancel(id)
|
|
202
|
+
__currentThreadHostTimerCancels.add(release)
|
|
203
|
+
const settle = (settleUnreleased) => (outcome) => {
|
|
204
|
+
__currentThreadHostTimerCancels.delete(release)
|
|
205
|
+
return __currentThreadHostTimersReleased
|
|
206
|
+
? new Promise(() => {})
|
|
207
|
+
: settleUnreleased(outcome)
|
|
208
|
+
}
|
|
209
|
+
return Promise.resolve(scheduled).then(
|
|
210
|
+
settle((value) => value),
|
|
211
|
+
settle((error) => {
|
|
212
|
+
throw error
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The binding as \`installCurrentThreadHosts\` should see it: every export
|
|
219
|
+
* reads through, and \`registerTimerHost\` hands the addon a \`schedule\` that
|
|
220
|
+
* records its \`cancel\`. A binding without a callable \`registerTimerHost\` is
|
|
221
|
+
* passed through, so the package reports the mismatch as before.
|
|
222
|
+
*/
|
|
223
|
+
function __trackCurrentThreadHostTimers(binding) {
|
|
224
|
+
let registerTimerHost
|
|
225
|
+
try {
|
|
226
|
+
registerTimerHost = binding.registerTimerHost
|
|
227
|
+
} catch {
|
|
228
|
+
return binding
|
|
229
|
+
}
|
|
230
|
+
if (typeof registerTimerHost !== 'function') {
|
|
231
|
+
return binding
|
|
232
|
+
}
|
|
233
|
+
const view = Object.create(binding)
|
|
234
|
+
Object.defineProperty(view, 'registerTimerHost', {
|
|
235
|
+
configurable: true,
|
|
236
|
+
enumerable: true,
|
|
237
|
+
writable: true,
|
|
238
|
+
value: function (...args) {
|
|
239
|
+
const [, , schedule, cancel] = args
|
|
240
|
+
if (typeof schedule === 'function' && typeof cancel === 'function') {
|
|
241
|
+
args[2] = (id, ms) =>
|
|
242
|
+
__scheduleCurrentThreadHostTimer(schedule, cancel, id, ms)
|
|
243
|
+
}
|
|
244
|
+
return Reflect.apply(registerTimerHost, this, args)
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
return view
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Clears every CurrentThread host timeout without entering wasm. Idempotent;
|
|
252
|
+
* a \`cancel\` that fails has already reported itself.
|
|
253
|
+
*/
|
|
254
|
+
function __releaseCurrentThreadHostTimers() {
|
|
255
|
+
__currentThreadHostTimersReleased = true
|
|
256
|
+
const releases = [...__currentThreadHostTimerCancels]
|
|
257
|
+
__currentThreadHostTimerCancels.clear()
|
|
258
|
+
for (const release of releases) {
|
|
259
|
+
try {
|
|
260
|
+
release()
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
`
|
|
265
|
+
: ''
|
|
125
266
|
const disposeCurrentThreadHosts = asyncRuntime
|
|
126
267
|
? ' __disposeCurrentThreadHosts()\n'
|
|
127
268
|
: ''
|
|
269
|
+
// After the in-flight check: a disposal that was already running when a
|
|
270
|
+
// thread died settles through the crash disposal itself, so every caller of
|
|
271
|
+
// it keeps getting the one promise it handed out.
|
|
272
|
+
const disposeAfterThreadCrash = threadCrashLatch
|
|
273
|
+
? ` if (!__wasiDisposed && __hasWasiThreadCrashed()) {
|
|
274
|
+
return __disposeWasiBindingAfterThreadCrash()
|
|
275
|
+
}
|
|
276
|
+
`
|
|
277
|
+
: ''
|
|
278
|
+
// A thread can die after the entry check above has passed. The disposal's
|
|
279
|
+
// polls and step boundaries check again, and a chain stopped that way — or
|
|
280
|
+
// failed any other way once a thread is dead — settles through the crash
|
|
281
|
+
// disposal. See `__abortWasiDisposalIfThreadCrashed`.
|
|
282
|
+
const abortDisposalAfterThreadCrash = (indent: string) =>
|
|
283
|
+
threadCrashLatch ? `${indent}__abortWasiDisposalIfThreadCrashed()\n` : ''
|
|
284
|
+
const settleDisposalAfterThreadCrash = (indent: string, exit: string) =>
|
|
285
|
+
threadCrashLatch
|
|
286
|
+
? `${indent}if (__settleWasiDisposalAfterThreadCrash(resolveDispose, rejectDispose)) {
|
|
287
|
+
${indent} ${exit}
|
|
288
|
+
${indent}}
|
|
289
|
+
`
|
|
290
|
+
: ''
|
|
291
|
+
// The barrier's poll ends in `…_finish`, which joins the runtime's work. A
|
|
292
|
+
// dead thread's work never goes idle, so after a crash the join would block
|
|
293
|
+
// this thread for good: leave the barrier parked and stop the disposal.
|
|
294
|
+
const finishWasmEnvCleanupUnlessCrashed = threadCrashLatch
|
|
295
|
+
? ` const finishCleanupUnlessCrashed = () => {
|
|
296
|
+
try {
|
|
297
|
+
__abortWasiDisposalIfThreadCrashed()
|
|
298
|
+
} catch (error) {
|
|
299
|
+
__finishParkedWasmEnvCleanup = undefined
|
|
300
|
+
throw error
|
|
301
|
+
}
|
|
302
|
+
finishCleanup()
|
|
303
|
+
}
|
|
304
|
+
`
|
|
305
|
+
: ''
|
|
306
|
+
const finishWasmEnvCleanupAfterPoll = threadCrashLatch
|
|
307
|
+
? 'finishCleanupUnlessCrashed'
|
|
308
|
+
: 'finishCleanup'
|
|
309
|
+
// The initialization rollback runs the same polls and steps. With the latch,
|
|
310
|
+
// its body keeps its own name and `__rollbackWasiInitialization` becomes the
|
|
311
|
+
// wrapper that stops it after a crash. See
|
|
312
|
+
// `__rollbackWasiInitializationAfterThreadCrash`.
|
|
313
|
+
const rollbackStepsName = threadCrashLatch
|
|
314
|
+
? '__runWasiInitializationRollbackSteps'
|
|
315
|
+
: '__rollbackWasiInitialization'
|
|
316
|
+
const rollbackAfterThreadCrash = threadCrashLatch
|
|
317
|
+
? `
|
|
318
|
+
/**
|
|
319
|
+
* The rollback above, stopped when a wasm thread has died.
|
|
320
|
+
*
|
|
321
|
+
* Its polls wait for work the dead thread still counts, so they never end, and
|
|
322
|
+
* the barrier's \`…_finish\` and \`Context.destroy()\` would re-enter wasm
|
|
323
|
+
* and wait on that thread for good. While the rollback runs, the poll turns
|
|
324
|
+
* and step boundaries stop it (\`__abortWasiDisposalIfThreadCrashed\`), and a
|
|
325
|
+
* crash seen before, during or after it ends it through
|
|
326
|
+
* \`__rollbackWasiInitializationAfterThreadCrash\`. Without a crash the
|
|
327
|
+
* result — synchronous or not — is passed through unchanged.
|
|
328
|
+
*/
|
|
329
|
+
function __rollbackWasiInitialization() {
|
|
330
|
+
if (__hasWasiThreadCrashed()) {
|
|
331
|
+
return __rollbackWasiInitializationAfterThreadCrash()
|
|
332
|
+
}
|
|
333
|
+
__wasiInitializationRollbackActive = true
|
|
334
|
+
let result
|
|
335
|
+
try {
|
|
336
|
+
result = __runWasiInitializationRollbackSteps()
|
|
337
|
+
} catch (error) {
|
|
338
|
+
__wasiInitializationRollbackActive = false
|
|
339
|
+
if (__hasWasiThreadCrashed()) {
|
|
340
|
+
return __rollbackWasiInitializationAfterThreadCrash()
|
|
341
|
+
}
|
|
342
|
+
throw error
|
|
343
|
+
}
|
|
344
|
+
if (!__isThenable(result)) {
|
|
345
|
+
__wasiInitializationRollbackActive = false
|
|
346
|
+
return __hasWasiThreadCrashed()
|
|
347
|
+
? __rollbackWasiInitializationAfterThreadCrash()
|
|
348
|
+
: result
|
|
349
|
+
}
|
|
350
|
+
return Promise.resolve(result).then(
|
|
351
|
+
(cleanupErrors) => {
|
|
352
|
+
__wasiInitializationRollbackActive = false
|
|
353
|
+
return __hasWasiThreadCrashed()
|
|
354
|
+
? __rollbackWasiInitializationAfterThreadCrash()
|
|
355
|
+
: cleanupErrors
|
|
356
|
+
},
|
|
357
|
+
(error) => {
|
|
358
|
+
__wasiInitializationRollbackActive = false
|
|
359
|
+
if (__hasWasiThreadCrashed()) {
|
|
360
|
+
return __rollbackWasiInitializationAfterThreadCrash()
|
|
361
|
+
}
|
|
362
|
+
throw error
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
`
|
|
367
|
+
: ''
|
|
128
368
|
|
|
129
369
|
return `
|
|
130
370
|
const __wasiDisposeSymbol = Symbol.for('${WASI_DISPOSE_SYMBOL}')
|
|
@@ -188,7 +428,7 @@ let __completeWasiDisposal = function () {}
|
|
|
188
428
|
// that stopped short of destroying the context. See
|
|
189
429
|
// \`__rollbackWasiInitialization\`.
|
|
190
430
|
let __retainWasiRollbackForRetry = function () {}
|
|
191
|
-
${currentThreadHosts}
|
|
431
|
+
${currentThreadHosts}${currentThreadHostTimersAfterThreadCrash}
|
|
192
432
|
function __isThenable(value) {
|
|
193
433
|
return (
|
|
194
434
|
value !== null &&
|
|
@@ -653,6 +893,7 @@ function __prepareWasmEnvCleanupWithTurns() {
|
|
|
653
893
|
// Publish the closer before yielding: from here until \`finishCleanup\` runs,
|
|
654
894
|
// a caller that cannot yield is entitled to end this handshake itself.
|
|
655
895
|
__finishParkedWasmEnvCleanup = finishCleanup
|
|
896
|
+
${finishWasmEnvCleanupUnlessCrashed}\
|
|
656
897
|
return (async () => {
|
|
657
898
|
// Unbounded, exactly like the async-work drain below. The wait ends when
|
|
658
899
|
// the addon reports its runtime work finished; the turns spent here are
|
|
@@ -660,6 +901,7 @@ function __prepareWasmEnvCleanupWithTurns() {
|
|
|
660
901
|
const pace = __createWasmRuntimePollPace()
|
|
661
902
|
for (;;) {
|
|
662
903
|
await __yieldWasmRuntimePollTurn(pace)
|
|
904
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
663
905
|
try {
|
|
664
906
|
if (!workPending()) {
|
|
665
907
|
return
|
|
@@ -670,7 +912,7 @@ function __prepareWasmEnvCleanupWithTurns() {
|
|
|
670
912
|
return
|
|
671
913
|
}
|
|
672
914
|
}
|
|
673
|
-
})().then(
|
|
915
|
+
})().then(${finishWasmEnvCleanupAfterPoll}, ${finishWasmEnvCleanupAfterPoll})
|
|
674
916
|
}
|
|
675
917
|
|
|
676
918
|
// Turns to wait for while the addon still reports queued settlements. Reaching
|
|
@@ -994,6 +1236,7 @@ function __drainWasiAsyncWork() {
|
|
|
994
1236
|
await new Promise((resolve) => {
|
|
995
1237
|
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)
|
|
996
1238
|
})
|
|
1239
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
997
1240
|
}
|
|
998
1241
|
})(),
|
|
999
1242
|
).then(
|
|
@@ -1089,6 +1332,7 @@ function __finishWasiDisposal() {
|
|
|
1089
1332
|
}
|
|
1090
1333
|
|
|
1091
1334
|
function __continueWasiDisposal() {
|
|
1335
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1092
1336
|
const destroyResult = __destroyEmnapiContext()
|
|
1093
1337
|
if (__isThenable(destroyResult)) {
|
|
1094
1338
|
return Promise.resolve(destroyResult).then(__finishWasiDisposal)
|
|
@@ -1097,6 +1341,7 @@ function __continueWasiDisposal() {
|
|
|
1097
1341
|
}
|
|
1098
1342
|
|
|
1099
1343
|
function __drainWasmEnvForWasiDisposal() {
|
|
1344
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1100
1345
|
const drainResult = __drainWasmEnvCleanup()
|
|
1101
1346
|
if (__isThenable(drainResult)) {
|
|
1102
1347
|
return Promise.resolve(drainResult).then(__continueWasiDisposal)
|
|
@@ -1105,6 +1350,7 @@ function __drainWasmEnvForWasiDisposal() {
|
|
|
1105
1350
|
}
|
|
1106
1351
|
|
|
1107
1352
|
function __cleanUpWasmEnvForWasiDisposal() {
|
|
1353
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1108
1354
|
// Run the pre-teardown barrier — yielding the turns its two-phase form asks
|
|
1109
1355
|
// for, when the addon has one — then let the settlements it queued actually
|
|
1110
1356
|
// reach JavaScript, and only then destroy the environment. Doing any two of
|
|
@@ -1141,6 +1387,7 @@ function __disposeWasiBinding() {
|
|
|
1141
1387
|
if (__wasiDisposePromise) {
|
|
1142
1388
|
return __wasiDisposePromise
|
|
1143
1389
|
}
|
|
1390
|
+
${disposeAfterThreadCrash}\
|
|
1144
1391
|
if (__wasiDisposed) {
|
|
1145
1392
|
return Promise.resolve()
|
|
1146
1393
|
}
|
|
@@ -1157,6 +1404,7 @@ function __disposeWasiBinding() {
|
|
|
1157
1404
|
try {
|
|
1158
1405
|
result = __startWasiDisposal()
|
|
1159
1406
|
} catch (error) {
|
|
1407
|
+
${settleDisposalAfterThreadCrash(' ', 'return disposePromise')}\
|
|
1160
1408
|
__wasiDisposePromise = undefined
|
|
1161
1409
|
rejectDispose(error)
|
|
1162
1410
|
return disposePromise
|
|
@@ -1168,6 +1416,7 @@ function __disposeWasiBinding() {
|
|
|
1168
1416
|
resolveDispose(value)
|
|
1169
1417
|
},
|
|
1170
1418
|
(error) => {
|
|
1419
|
+
${settleDisposalAfterThreadCrash(' ', 'return')}\
|
|
1171
1420
|
__wasiDisposePromise = undefined
|
|
1172
1421
|
rejectDispose(error)
|
|
1173
1422
|
},
|
|
@@ -1203,6 +1452,7 @@ function __finishWasiInitializationRollback(cleanupErrors) {
|
|
|
1203
1452
|
}
|
|
1204
1453
|
|
|
1205
1454
|
function __destroyContextForWasiRollback(cleanupErrors) {
|
|
1455
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1206
1456
|
let destroyResult
|
|
1207
1457
|
try {
|
|
1208
1458
|
destroyResult = __destroyEmnapiContext()
|
|
@@ -1271,10 +1521,11 @@ function __retainFailedWasiRollback(cleanupErrors) {
|
|
|
1271
1521
|
* goes away. That is the deliberate choice: a hung promise is a silent liveness
|
|
1272
1522
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.
|
|
1273
1523
|
*/
|
|
1274
|
-
function
|
|
1524
|
+
function ${rollbackStepsName}() {
|
|
1275
1525
|
// The environment teardown this rollback performs, kept nested so it cannot
|
|
1276
1526
|
// be reached without the async-work drain below running first.
|
|
1277
1527
|
function __rollbackWasmEnvForWasiInitialization() {
|
|
1528
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1278
1529
|
const cleanupErrors = []
|
|
1279
1530
|
let prepareResult
|
|
1280
1531
|
try {
|
|
@@ -1300,6 +1551,7 @@ function __rollbackWasiInitialization() {
|
|
|
1300
1551
|
// did not finish never gets here: it retains instead, exactly as a drain that
|
|
1301
1552
|
// did not finish does.
|
|
1302
1553
|
function __drainWasmEnvForWasiRollback(cleanupErrors) {
|
|
1554
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1303
1555
|
let drainResult
|
|
1304
1556
|
try {
|
|
1305
1557
|
drainResult = __drainWasmEnvCleanup()
|
|
@@ -1346,6 +1598,7 @@ function __rollbackWasiInitialization() {
|
|
|
1346
1598
|
}
|
|
1347
1599
|
return __rollbackWasmEnvForWasiInitialization()
|
|
1348
1600
|
}
|
|
1601
|
+
${rollbackAfterThreadCrash}\
|
|
1349
1602
|
`
|
|
1350
1603
|
}
|
|
1351
1604
|
|
|
@@ -1499,10 +1752,35 @@ const __workerPoolSize = Math.max(
|
|
|
1499
1752
|
type: 'module',
|
|
1500
1753
|
})
|
|
1501
1754
|
__wasiWorkers.add(worker)
|
|
1755
|
+
__shareWasiAddonCrashFlag(worker)
|
|
1502
1756
|
${workerFsHandler}
|
|
1503
1757
|
${workerErrorHandler}
|
|
1504
1758
|
return worker
|
|
1505
1759
|
},
|
|
1760
|
+
`
|
|
1761
|
+
: ''
|
|
1762
|
+
// The pool is created before the wasm is instantiated, so the workers that
|
|
1763
|
+
// exist by `beforeInit` get the view there, after their 'load' message and
|
|
1764
|
+
// before any 'start'; a worker created later gets it first thing. A message
|
|
1765
|
+
// rather than worker options: a browser Worker has no `workerData`.
|
|
1766
|
+
const addonCrashFlagSharing = threads
|
|
1767
|
+
? `${ADDON_CRASH_FLAG_CAPTURE}
|
|
1768
|
+
function __shareWasiAddonCrashFlag(worker) {
|
|
1769
|
+
if (__wasiAddonCrashFlag === undefined) {
|
|
1770
|
+
return
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
worker.postMessage({ __napiRsAddonCrashFlag: __wasiAddonCrashFlag })
|
|
1774
|
+
} catch {}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
`
|
|
1778
|
+
: ''
|
|
1779
|
+
const captureAddonCrashFlag = threads
|
|
1780
|
+
? ` __captureWasiAddonCrashFlag(instance)
|
|
1781
|
+
for (const worker of __wasiWorkers) {
|
|
1782
|
+
__shareWasiAddonCrashFlag(worker)
|
|
1783
|
+
}
|
|
1506
1784
|
`
|
|
1507
1785
|
: ''
|
|
1508
1786
|
|
|
@@ -1542,6 +1820,7 @@ ${threads ? ' shared: true,\n' : ''}\
|
|
|
1542
1820
|
${workerPoolSizeBinding}\
|
|
1543
1821
|
let __emnapiContext
|
|
1544
1822
|
${createEmnapiContextLifecycle(asyncRuntime)}
|
|
1823
|
+
${addonCrashFlagSharing}\
|
|
1545
1824
|
let __wasiModule
|
|
1546
1825
|
let __napiModule
|
|
1547
1826
|
|
|
@@ -1575,6 +1854,7 @@ ${workerOption}\
|
|
|
1575
1854
|
},
|
|
1576
1855
|
beforeInit({ instance }) {
|
|
1577
1856
|
__napiInstance = instance
|
|
1857
|
+
${captureAddonCrashFlag}\
|
|
1578
1858
|
for (const name of Object.keys(instance.exports)) {
|
|
1579
1859
|
if (name.startsWith('__napi_register__')) {
|
|
1580
1860
|
instance.exports[name]()
|
|
@@ -3608,9 +3888,11 @@ export const createWasiBinding = (
|
|
|
3608
3888
|
} = require('@napi-rs/async-runtime')
|
|
3609
3889
|
`
|
|
3610
3890
|
: ''
|
|
3891
|
+
// The threaded loader has the thread crash latch, whose disposal releases
|
|
3892
|
+
// the host timers it tracks. See \`__trackCurrentThreadHostTimers\`.
|
|
3611
3893
|
const installAsyncRuntimeHosts = asyncRuntime
|
|
3612
3894
|
? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
|
|
3613
|
-
__napiModule.exports,
|
|
3895
|
+
${threads ? '__trackCurrentThreadHostTimers(__napiModule.exports)' : '__napiModule.exports'},
|
|
3614
3896
|
)
|
|
3615
3897
|
`
|
|
3616
3898
|
: ''
|
|
@@ -3694,7 +3976,13 @@ function __createWasiWorker(filename) {
|
|
|
3694
3976
|
return new Worker(filename, {
|
|
3695
3977
|
env: process.env,
|
|
3696
3978
|
execArgv: __workerExecArgv,
|
|
3697
|
-
workerData: {
|
|
3979
|
+
workerData: {
|
|
3980
|
+
hostRoot: __hostRoot,
|
|
3981
|
+
rootDir: __rootDir,
|
|
3982
|
+
crashFlag: __wasiThreadCrashFlag,
|
|
3983
|
+
crashReport: __wasiThreadCrashReport,
|
|
3984
|
+
addonCrashFlag: __wasiAddonCrashFlag,
|
|
3985
|
+
},
|
|
3698
3986
|
})
|
|
3699
3987
|
} catch (error) {
|
|
3700
3988
|
if (!error || error.code !== 'ERR_WORKER_INVALID_EXEC_ARGV') {
|
|
@@ -3709,6 +3997,306 @@ function __createWasiWorker(filename) {
|
|
|
3709
3997
|
}
|
|
3710
3998
|
}
|
|
3711
3999
|
}
|
|
4000
|
+
`
|
|
4001
|
+
: ''
|
|
4002
|
+
// The host timeouts a CurrentThread sleep armed, released without entering
|
|
4003
|
+
// wasm on every crash path. See `__trackCurrentThreadHostTimers`.
|
|
4004
|
+
const releaseCurrentThreadHostTimers = (indent: string) =>
|
|
4005
|
+
threads && asyncRuntime
|
|
4006
|
+
? `${indent}__releaseCurrentThreadHostTimers()\n`
|
|
4007
|
+
: ''
|
|
4008
|
+
// Only the threaded flavor has pool workers whose wasm thread can die under
|
|
4009
|
+
// this one. See `__disposeWasiBindingAtExit`.
|
|
4010
|
+
const threadCrashLatch = threads
|
|
4011
|
+
? `
|
|
4012
|
+
// Set to 1 by a pool worker (see wasi-worker.mjs) right before it reports that
|
|
4013
|
+
// its wasm thread died. Shared memory, so this thread reads it synchronously even
|
|
4014
|
+
// while the worker's 'error' event is still queued behind the code that is
|
|
4015
|
+
// exiting right now.
|
|
4016
|
+
const __wasiThreadCrashFlag = new Int32Array(new SharedArrayBuffer(4))
|
|
4017
|
+
// Written by the first pool worker whose wasm thread dies, before it raises the
|
|
4018
|
+
// flag: its error and \`threadId\` (layout in wasi-worker.mjs). Read without
|
|
4019
|
+
// waiting for any event, see \`__readWasiThreadCrashReport\`.
|
|
4020
|
+
const __wasiThreadCrashReport = new SharedArrayBuffer(4096)
|
|
4021
|
+
let __wasiThreadCrashReportRead
|
|
4022
|
+
// The first error a pool worker reported through its 'error' event, and that
|
|
4023
|
+
// worker's \`threadId\`. See \`__getWasiThreadCrashError\`.
|
|
4024
|
+
let __wasiThreadCrashWorkerError
|
|
4025
|
+
let __wasiThreadCrashWorkerId
|
|
4026
|
+
let __wasiThreadCrashError
|
|
4027
|
+
// Raised while \`__runWasiInitializationRollbackSteps\` runs, so its polls and
|
|
4028
|
+
// steps stop after a crash the way a public disposal's do.
|
|
4029
|
+
let __wasiInitializationRollbackActive = false
|
|
4030
|
+
let __wasiThreadCrashed = false
|
|
4031
|
+
|
|
4032
|
+
${ADDON_CRASH_FLAG_CAPTURE}
|
|
4033
|
+
/**
|
|
4034
|
+
* Whether any wasm thread of this binding has died: a trap or an uncaught error
|
|
4035
|
+
* in a pool worker, including one that failed to load after its thread spawn
|
|
4036
|
+
* had already been reported as started.
|
|
4037
|
+
*
|
|
4038
|
+
* After that the shared wasm state cannot be trusted: a lock the dead thread
|
|
4039
|
+
* held stays held, and a join or park that waits on it — the async runtime's
|
|
4040
|
+
* \`finish_shutdown\` waiting for the dead thread's work to go idle — blocks
|
|
4041
|
+
* this thread forever in a raw \`memory.atomic.wait32\`, which neither
|
|
4042
|
+
* emnapi's crash check nor a signal can interrupt.
|
|
4043
|
+
*/
|
|
4044
|
+
function __hasWasiThreadCrashed() {
|
|
4045
|
+
if (__wasiThreadCrashed || Atomics.load(__wasiThreadCrashFlag, 0) !== 0) {
|
|
4046
|
+
return true
|
|
4047
|
+
}
|
|
4048
|
+
const manager = __getWasiThreadManager()
|
|
4049
|
+
return Boolean(manager && manager._fatalError)
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
let __wasiThreadCrashDisposePromise
|
|
4053
|
+
|
|
4054
|
+
/**
|
|
4055
|
+
* Stores the first error a pool worker reported, with the worker's id. Called
|
|
4056
|
+
* from the loader's own 'error' listener, which runs before emnapi's.
|
|
4057
|
+
*/
|
|
4058
|
+
function __recordWasiThreadCrashError(error, workerId) {
|
|
4059
|
+
if (__wasiThreadCrashWorkerError !== undefined || error === undefined) {
|
|
4060
|
+
return
|
|
4061
|
+
}
|
|
4062
|
+
__wasiThreadCrashWorkerError = error
|
|
4063
|
+
__wasiThreadCrashWorkerId = workerId
|
|
4064
|
+
__fillWasiThreadCrashError()
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
/**
|
|
4068
|
+
* The error in the shared crash report, rebuilt once it is complete: the
|
|
4069
|
+
* worker writes it before it raises the flag, so it is there as soon as the
|
|
4070
|
+
* crash is seen. Plain JavaScript over shared memory; never enters wasm.
|
|
4071
|
+
*/
|
|
4072
|
+
function __readWasiThreadCrashReport() {
|
|
4073
|
+
if (__wasiThreadCrashReportRead !== undefined) {
|
|
4074
|
+
return __wasiThreadCrashReportRead
|
|
4075
|
+
}
|
|
4076
|
+
try {
|
|
4077
|
+
const header = new Int32Array(__wasiThreadCrashReport, 0, 3)
|
|
4078
|
+
if (Atomics.load(header, 0) !== 2) {
|
|
4079
|
+
return
|
|
4080
|
+
}
|
|
4081
|
+
const length = Atomics.load(header, 1)
|
|
4082
|
+
const threadId = Atomics.load(header, 2)
|
|
4083
|
+
let error
|
|
4084
|
+
if (length > 0) {
|
|
4085
|
+
// Copied out of shared memory: TextDecoder does not take a shared view.
|
|
4086
|
+
const bytes = new Uint8Array(__wasiThreadCrashReport, 12, length).slice()
|
|
4087
|
+
const report = JSON.parse(new TextDecoder().decode(bytes))
|
|
4088
|
+
error = new Error(String(report.message))
|
|
4089
|
+
if (typeof report.name === 'string') {
|
|
4090
|
+
error.name = report.name
|
|
4091
|
+
}
|
|
4092
|
+
if (typeof report.stack === 'string') {
|
|
4093
|
+
error.stack = report.stack
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
__wasiThreadCrashReportRead = { error, threadId }
|
|
4097
|
+
} catch {
|
|
4098
|
+
__wasiThreadCrashReportRead = { error: undefined, threadId: 0 }
|
|
4099
|
+
}
|
|
4100
|
+
return __wasiThreadCrashReportRead
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
/**
|
|
4104
|
+
* The one error every crash path of this binding reports, created on first
|
|
4105
|
+
* use.
|
|
4106
|
+
*
|
|
4107
|
+
* The shared flag is raised before this thread has processed the worker's
|
|
4108
|
+
* 'error' event, and once the workers are terminated that event is often
|
|
4109
|
+
* never delivered, so neither it nor emnapi's \`_fatalError\` can be counted
|
|
4110
|
+
* on. The cause is filled in from the first source that has it: the error the
|
|
4111
|
+
* 'error' listener kept, else the worker's shared crash report, else
|
|
4112
|
+
* \`_fatalError\` — here, and again from the 'error' listener when it arrives
|
|
4113
|
+
* later. Until then the error carries only its message.
|
|
4114
|
+
*/
|
|
4115
|
+
function __getWasiThreadCrashError() {
|
|
4116
|
+
if (__wasiThreadCrashError === undefined) {
|
|
4117
|
+
__wasiThreadCrashError = new Error(
|
|
4118
|
+
'napi-rs: WASI binding cannot be disposed after a worker thread crashed',
|
|
4119
|
+
)
|
|
4120
|
+
}
|
|
4121
|
+
__fillWasiThreadCrashError()
|
|
4122
|
+
return __wasiThreadCrashError
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function __fillWasiThreadCrashError() {
|
|
4126
|
+
const crashError = __wasiThreadCrashError
|
|
4127
|
+
if (crashError === undefined) {
|
|
4128
|
+
return
|
|
4129
|
+
}
|
|
4130
|
+
try {
|
|
4131
|
+
if (crashError.cause === undefined) {
|
|
4132
|
+
let cause = __wasiThreadCrashWorkerError
|
|
4133
|
+
if (cause === undefined) {
|
|
4134
|
+
const report = __readWasiThreadCrashReport()
|
|
4135
|
+
cause = report ? report.error : undefined
|
|
4136
|
+
}
|
|
4137
|
+
if (cause === undefined) {
|
|
4138
|
+
const manager = __getWasiThreadManager()
|
|
4139
|
+
cause = manager ? manager._fatalError : undefined
|
|
4140
|
+
}
|
|
4141
|
+
if (cause !== undefined && cause !== null) {
|
|
4142
|
+
crashError.cause = cause
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (crashError.workerThreadId === undefined) {
|
|
4146
|
+
let workerId = __wasiThreadCrashWorkerId
|
|
4147
|
+
if (workerId === undefined) {
|
|
4148
|
+
const report = __readWasiThreadCrashReport()
|
|
4149
|
+
workerId = report && report.threadId > 0 ? report.threadId : undefined
|
|
4150
|
+
}
|
|
4151
|
+
if (workerId !== undefined) {
|
|
4152
|
+
crashError.workerThreadId = workerId
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
} catch {}
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
/**
|
|
4159
|
+
* Let the event loop drain after a wasm thread died, without entering wasm.
|
|
4160
|
+
*
|
|
4161
|
+
* emnapi's \`Context\` keeps a \`NodejsWaitingRequestCounter\` on Node: a
|
|
4162
|
+
* \`MessagePort\` (\`refCounter.refHandle\`) that it refs when the count of
|
|
4163
|
+
* in-flight async work and threadsafe-function requests leaves zero and unrefs
|
|
4164
|
+
* when it comes back. The requests the dead thread held never complete, so the
|
|
4165
|
+
* count never returns to zero and the port holds the process open for good.
|
|
4166
|
+
* \`Context.destroy()\` would not release it either — and it runs cleanup
|
|
4167
|
+
* hooks, which is exactly what must not happen now. So unref the port directly.
|
|
4168
|
+
* The count is left as is: it stays above zero, so a later request never refs
|
|
4169
|
+
* the port again.
|
|
4170
|
+
*
|
|
4171
|
+
* The field is private in emnapi's typings, so read it defensively; a context
|
|
4172
|
+
* without it (a non-Node host, a future emnapi) is left alone.
|
|
4173
|
+
*/
|
|
4174
|
+
function __releaseEmnapiWaitingRequestHandle() {
|
|
4175
|
+
try {
|
|
4176
|
+
const refCounter = __emnapiContext && __emnapiContext.refCounter
|
|
4177
|
+
const refHandle = refCounter && refCounter.refHandle
|
|
4178
|
+
if (refHandle && typeof refHandle.unref === 'function') {
|
|
4179
|
+
refHandle.unref()
|
|
4180
|
+
}
|
|
4181
|
+
} catch {}
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
/**
|
|
4185
|
+
* The public disposer after a wasm thread died. The normal chain drains async
|
|
4186
|
+
* work, runs the environment cleanup barrier and destroys the context — every
|
|
4187
|
+
* one of those re-enters wasm, and the barrier's shutdown waits for the dead
|
|
4188
|
+
* thread's work in the same raw atomic wait \`__disposeWasiBindingAtExit\`
|
|
4189
|
+
* avoids. An app that handled the worker's error and then disposes would block
|
|
4190
|
+
* there for good. Only stop the workers, then reject: the binding was not
|
|
4191
|
+
* cleaned up and cannot be, so reporting success would be a lie. The context
|
|
4192
|
+
* is not destroyed — only its waiting-request port is unrefed, so the process
|
|
4193
|
+
* can exit on its own — and the 'exit' listener takes its short path too.
|
|
4194
|
+
*
|
|
4195
|
+
* Latched: every later call returns the same promise.
|
|
4196
|
+
*/
|
|
4197
|
+
function __disposeWasiBindingAfterThreadCrash() {
|
|
4198
|
+
if (__wasiThreadCrashDisposePromise) {
|
|
4199
|
+
return __wasiThreadCrashDisposePromise
|
|
4200
|
+
}
|
|
4201
|
+
__releaseEmnapiWaitingRequestHandle()
|
|
4202
|
+
${releaseCurrentThreadHostTimers(' ')}\
|
|
4203
|
+
let workerResult
|
|
4204
|
+
try {
|
|
4205
|
+
workerResult = __terminateWasiWorkers()
|
|
4206
|
+
} catch (terminateError) {
|
|
4207
|
+
workerResult = Promise.reject(terminateError)
|
|
4208
|
+
}
|
|
4209
|
+
// Built at settlement, after the workers stopped: by then the 'error' event
|
|
4210
|
+
// has usually delivered the worker's error.
|
|
4211
|
+
__wasiThreadCrashDisposePromise = Promise.resolve(workerResult).then(
|
|
4212
|
+
() => {
|
|
4213
|
+
throw __getWasiThreadCrashError()
|
|
4214
|
+
},
|
|
4215
|
+
(terminateError) => {
|
|
4216
|
+
throw __attachCleanupErrors(__getWasiThreadCrashError(), [
|
|
4217
|
+
terminateError,
|
|
4218
|
+
])
|
|
4219
|
+
},
|
|
4220
|
+
)
|
|
4221
|
+
return __wasiThreadCrashDisposePromise
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
/**
|
|
4225
|
+
* Stops a public disposal that a thread crash overtook.
|
|
4226
|
+
*
|
|
4227
|
+
* The check at the top of \`__disposeWasiBinding\` only sees a crash that came
|
|
4228
|
+
* first. A thread that dies once the chain is running leaves it polling for
|
|
4229
|
+
* work the dead thread still counts — the async-work drain for
|
|
4230
|
+
* \`napi_wasm_async_work_pending\`, the barrier's poll for
|
|
4231
|
+
* \`napi_wasm_runtime_work_pending\` — and neither count ever reaches zero, so
|
|
4232
|
+
* the poll's referenced timers keep the process alive forever with the
|
|
4233
|
+
* disposal promise pending. Each poll turn and each step boundary calls this,
|
|
4234
|
+
* and the throw ends the chain before anything re-enters wasm again; the
|
|
4235
|
+
* disposer then settles it through \`__settleWasiDisposalAfterThreadCrash\`.
|
|
4236
|
+
*
|
|
4237
|
+
* The initialization rollback runs the same polls and steps, so they stop while
|
|
4238
|
+
* it runs too; its wrapper, \`__rollbackWasiInitialization\`, then ends it
|
|
4239
|
+
* through \`__rollbackWasiInitializationAfterThreadCrash\`. A bare poll with
|
|
4240
|
+
* neither in flight is left alone.
|
|
4241
|
+
*/
|
|
4242
|
+
function __abortWasiDisposalIfThreadCrashed() {
|
|
4243
|
+
if (
|
|
4244
|
+
(__wasiDisposePromise !== undefined ||
|
|
4245
|
+
__wasiInitializationRollbackActive) &&
|
|
4246
|
+
__hasWasiThreadCrashed()
|
|
4247
|
+
) {
|
|
4248
|
+
throw new Error(
|
|
4249
|
+
'napi-rs: WASI disposal stopped because a worker thread crashed',
|
|
4250
|
+
)
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
/**
|
|
4255
|
+
* Settles an in-flight public disposal whose chain failed after a thread
|
|
4256
|
+
* died — stopped by \`__abortWasiDisposalIfThreadCrashed\` or failing any other
|
|
4257
|
+
* way — through the crash disposal: it releases the waiting-request port,
|
|
4258
|
+
* terminates the workers and rejects with the same error an entry-time crash
|
|
4259
|
+
* gets. \`__wasiDisposePromise\` stays set, so this caller, every caller that
|
|
4260
|
+
* joined it and every later one hold the same promise. Returns false when no
|
|
4261
|
+
* thread died, leaving the ordinary failure handling alone.
|
|
4262
|
+
*/
|
|
4263
|
+
function __settleWasiDisposalAfterThreadCrash(resolve, reject) {
|
|
4264
|
+
if (!__hasWasiThreadCrashed()) {
|
|
4265
|
+
return false
|
|
4266
|
+
}
|
|
4267
|
+
__disposeWasiBindingAfterThreadCrash().then(resolve, reject)
|
|
4268
|
+
return true
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
/**
|
|
4272
|
+
* Ends an initialization rollback after a wasm thread died, without entering
|
|
4273
|
+
* wasm: the barrier, its finish and \`Context.destroy()\` are skipped, and the
|
|
4274
|
+
* crash disposal releases the waiting-request port and terminates the workers,
|
|
4275
|
+
* once. The initialization error itself is left to propagate: the crash error
|
|
4276
|
+
* is returned as the rollback's cleanup error, so
|
|
4277
|
+
* \`__completeWasiInitializationRollback\` attaches it — as the cause when the
|
|
4278
|
+
* error has none — and keeps the record, since the context was not destroyed.
|
|
4279
|
+
*/
|
|
4280
|
+
function __rollbackWasiInitializationAfterThreadCrash() {
|
|
4281
|
+
const crashError = __getWasiThreadCrashError()
|
|
4282
|
+
// Its rejection is \`crashError\` itself, which the caller reports.
|
|
4283
|
+
void __disposeWasiBindingAfterThreadCrash().catch(() => {})
|
|
4284
|
+
return [crashError]
|
|
4285
|
+
}
|
|
4286
|
+
`
|
|
4287
|
+
: ''
|
|
4288
|
+
const skipTeardownAfterThreadCrash = threads
|
|
4289
|
+
? ` if (__hasWasiThreadCrashed()) {
|
|
4290
|
+
// Never re-enter wasm after a thread died: the environment cleanup joins
|
|
4291
|
+
// the dead thread's work and would hang the exit forever — SIGTERM
|
|
4292
|
+
// included, since its JavaScript listener never gets a turn. Stop the
|
|
4293
|
+
// workers and leave.
|
|
4294
|
+
${releaseCurrentThreadHostTimers(' ')}\
|
|
4295
|
+
try {
|
|
4296
|
+
void Promise.resolve(__terminateWasiWorkers()).catch(() => {})
|
|
4297
|
+
} catch {}
|
|
4298
|
+
return
|
|
4299
|
+
}
|
|
3712
4300
|
`
|
|
3713
4301
|
: ''
|
|
3714
4302
|
const workerRuntimeImport = threads
|
|
@@ -3755,10 +4343,18 @@ function __createWasiWorker(filename) {
|
|
|
3755
4343
|
// with threads there is no JavaScript seam at all, so `__drainWasiAsyncWork`
|
|
3756
4344
|
// asks the addon instead.
|
|
3757
4345
|
const emnapiPluginRequire = ` emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin: __emnapiTSFNPlugin,\n`
|
|
4346
|
+
const captureAddonCrashFlag = threads
|
|
4347
|
+
? ' __captureWasiAddonCrashFlag(instance)\n'
|
|
4348
|
+
: ''
|
|
3758
4349
|
const workerOption = threads
|
|
3759
4350
|
? ` onCreateWorker() {
|
|
3760
4351
|
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
|
|
3761
4352
|
__wasiWorkers.add(worker)
|
|
4353
|
+
// Registered before emnapi's own listeners, which rethrow the error.
|
|
4354
|
+
worker.on('error', (error) => {
|
|
4355
|
+
__wasiThreadCrashed = true
|
|
4356
|
+
__recordWasiThreadCrashError(error, worker.threadId)
|
|
4357
|
+
})
|
|
3762
4358
|
worker.onmessage = ({ data }) => {
|
|
3763
4359
|
__wasmCreateOnMessageForFsProxy(__nodeFs)(data)
|
|
3764
4360
|
}
|
|
@@ -3813,6 +4409,7 @@ ${workerRuntimeImport}\
|
|
|
3813
4409
|
const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
|
|
3814
4410
|
${asyncRuntimeImport}\
|
|
3815
4411
|
${workerExecArgv}\
|
|
4412
|
+
${threadCrashLatch}\
|
|
3816
4413
|
|
|
3817
4414
|
const __cwd = process.cwd()
|
|
3818
4415
|
const __rootDir = __nodePath.parse(__cwd).root
|
|
@@ -3855,7 +4452,7 @@ if (__nodeFs.existsSync(__wasmDebugFilePath)) {
|
|
|
3855
4452
|
|
|
3856
4453
|
const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
|
|
3857
4454
|
let __emnapiContext
|
|
3858
|
-
${createEmnapiContextLifecycle(asyncRuntime)}
|
|
4455
|
+
${createEmnapiContextLifecycle(asyncRuntime, threads)}
|
|
3859
4456
|
const __wasiRollbackRegistrySymbol = Symbol.for('${WASI_ROLLBACK_REGISTRY_SYMBOL}')
|
|
3860
4457
|
const __wasiRollbackRegistryKey =
|
|
3861
4458
|
typeof __filename === 'string' ? __filename : __wasmFilePath
|
|
@@ -3959,6 +4556,7 @@ function __removeWasiExitListener() {
|
|
|
3959
4556
|
|
|
3960
4557
|
function __disposeWasiBindingAtExit() {
|
|
3961
4558
|
__wasiExitListenerRegistered = false
|
|
4559
|
+
${skipTeardownAfterThreadCrash}\
|
|
3962
4560
|
// An 'exit' handler cannot yield, so it cannot wait for queued promise
|
|
3963
4561
|
// settlements the way __startWasiDisposal does — the process is leaving and
|
|
3964
4562
|
// those promises have no observer left anyway. Run the synchronous teardown
|
|
@@ -4068,6 +4666,7 @@ ${workerOption}\
|
|
|
4068
4666
|
},
|
|
4069
4667
|
beforeInit({ instance }) {
|
|
4070
4668
|
__napiInstance = instance
|
|
4669
|
+
${captureAddonCrashFlag}\
|
|
4071
4670
|
for (const name of Object.keys(instance.exports)) {
|
|
4072
4671
|
if (name.startsWith('__napi_register__')) {
|
|
4073
4672
|
instance.exports[name]()
|