@napi-rs/cli 3.10.5 → 3.10.7
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 +969 -105
- package/dist/index.cjs +969 -105
- package/dist/index.d.cts +1 -1
- package/dist/index.js +969 -105
- package/docs/wasi.md +270 -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 +2618 -9
- package/src/api/templates/load-wasi-template.ts +819 -8
- package/src/api/templates/wasi-worker-template.ts +191 -63
|
@@ -5,8 +5,50 @@ import {
|
|
|
5
5
|
} from './binding-target.js'
|
|
6
6
|
|
|
7
7
|
const WASI_DISPOSE_SYMBOL = 'napi.rs.wasi.dispose'
|
|
8
|
+
const WASI_THREAD_POOL_RECONCILE_SYMBOL = 'napi.rs.wasi.reconcileThreadPool'
|
|
8
9
|
const WASI_ROLLBACK_REGISTRY_SYMBOL = 'napi.rs.wasi.rollback.registry.v1'
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Threaded loaders only: a view of the addon's crash flag for the pool workers.
|
|
13
|
+
*
|
|
14
|
+
* napi-async-runtime's shutdown waits run on the loader thread, inside the
|
|
15
|
+
* environment cleanup calls, and check one word of the shared wasm memory
|
|
16
|
+
* between 1 ms slices; once it is set they trap instead of waiting on a dead
|
|
17
|
+
* thread for good. napi's `napi_wasm_thread_crash_flag_address` export says
|
|
18
|
+
* where that word is. Each worker gets an `Int32Array` over it and raises it
|
|
19
|
+
* with `Atomics.store` when its wasm thread dies — right after the loader's own
|
|
20
|
+
* crash flag, where there is one. The store needs no instance, so a worker that
|
|
21
|
+
* fails while it loads, after the thread spawn that created it had already
|
|
22
|
+
* returned, raises it too.
|
|
23
|
+
*
|
|
24
|
+
* Read in `beforeInit`: before any registration code runs, and on Node before
|
|
25
|
+
* any pool worker exists (the pool is created on demand). An addon built with
|
|
26
|
+
* an older napi has no export, and a value that is not a 4-aligned address
|
|
27
|
+
* inside the memory is ignored: the view stays undefined and the waits stay
|
|
28
|
+
* unbounded, as before.
|
|
29
|
+
*/
|
|
30
|
+
const ADDON_CRASH_FLAG_CAPTURE = `// A view of the addon's crash flag, one word of the shared wasm memory, for the
|
|
31
|
+
// pool workers: they raise it when their wasm thread dies, and the shutdown
|
|
32
|
+
// waits inside the cleanup calls on this thread then trap instead of waiting on
|
|
33
|
+
// the dead thread for good. Undefined for an addon built with an older napi.
|
|
34
|
+
let __wasiAddonCrashFlag
|
|
35
|
+
|
|
36
|
+
function __captureWasiAddonCrashFlag(instance) {
|
|
37
|
+
try {
|
|
38
|
+
const getAddress = instance.exports.napi_wasm_thread_crash_flag_address
|
|
39
|
+
if (typeof getAddress !== 'function') {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const address = getAddress() >>> 0
|
|
43
|
+
const buffer = __sharedMemory.buffer
|
|
44
|
+
if (address === 0 || address % 4 !== 0 || address + 4 > buffer.byteLength) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
__wasiAddonCrashFlag = new Int32Array(buffer, address, 1)
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
`
|
|
51
|
+
|
|
10
52
|
/**
|
|
11
53
|
* `Context.destroy()` disables JavaScript calls *before* it runs cleanup hooks
|
|
12
54
|
* (`setStopping` -> `setCanCallIntoJs(false)` -> `runCleanup`), and the
|
|
@@ -88,7 +130,12 @@ function __wrapEmnapiContextDestroyForSettlement(
|
|
|
88
130
|
* `__startWasiDisposal` / the rollback have already prepared and drained, so
|
|
89
131
|
* one call there covers all three.
|
|
90
132
|
*/
|
|
91
|
-
const createEmnapiContextLifecycle = (
|
|
133
|
+
const createEmnapiContextLifecycle = (
|
|
134
|
+
asyncRuntime: boolean,
|
|
135
|
+
// Only the threaded Node loader defines the crash latch
|
|
136
|
+
// (`__hasWasiThreadCrashed`, `__disposeWasiBindingAfterThreadCrash`).
|
|
137
|
+
threadCrashLatch = false,
|
|
138
|
+
) => {
|
|
92
139
|
const currentThreadHosts = asyncRuntime
|
|
93
140
|
? `
|
|
94
141
|
let __currentThreadHostsDisposer
|
|
@@ -122,9 +169,203 @@ function __disposeCurrentThreadHosts() {
|
|
|
122
169
|
}
|
|
123
170
|
`
|
|
124
171
|
: ''
|
|
172
|
+
const currentThreadHostTimersAfterThreadCrash =
|
|
173
|
+
asyncRuntime && threadCrashLatch
|
|
174
|
+
? `
|
|
175
|
+
/**
|
|
176
|
+
* The CurrentThread timer host arms one referenced \`setTimeout\` per sleep in
|
|
177
|
+
* flight, so a long sleep holds the event loop open until it fires.
|
|
178
|
+
* \`__disposeCurrentThreadHosts\` releases them, but only
|
|
179
|
+
* \`__destroyEmnapiContext\` calls it, and the crash disposal never gets there:
|
|
180
|
+
* after a wasm thread died it must not enter wasm, and both the host
|
|
181
|
+
* unregister calls and a settled timer promise do. A dispose() that rejected
|
|
182
|
+
* after a crash then left the process alive until the longest sleep ended.
|
|
183
|
+
*
|
|
184
|
+
* So the loader keeps the package's own \`cancel\` for every timer in flight,
|
|
185
|
+
* and \`__releaseCurrentThreadHostTimers\` calls them after a crash. \`cancel\`
|
|
186
|
+
* is plain JavaScript: it clears the host timeout and resolves the package's
|
|
187
|
+
* promise. The promise the addon holds is the one below, and once released it
|
|
188
|
+
* never settles, so nothing is handed back to wasm. A sleep armed after that
|
|
189
|
+
* is never armed at all.
|
|
190
|
+
*/
|
|
191
|
+
const __currentThreadHostTimerCancels = new Set()
|
|
192
|
+
let __currentThreadHostTimersReleased = false
|
|
193
|
+
|
|
194
|
+
function __scheduleCurrentThreadHostTimer(schedule, cancel, id, ms) {
|
|
195
|
+
if (__currentThreadHostTimersReleased) {
|
|
196
|
+
return new Promise(() => {})
|
|
197
|
+
}
|
|
198
|
+
const scheduled = schedule(id, ms)
|
|
199
|
+
if (!__isThenable(scheduled)) {
|
|
200
|
+
return scheduled
|
|
201
|
+
}
|
|
202
|
+
const release = () => cancel(id)
|
|
203
|
+
__currentThreadHostTimerCancels.add(release)
|
|
204
|
+
const settle = (settleUnreleased) => (outcome) => {
|
|
205
|
+
__currentThreadHostTimerCancels.delete(release)
|
|
206
|
+
return __currentThreadHostTimersReleased
|
|
207
|
+
? new Promise(() => {})
|
|
208
|
+
: settleUnreleased(outcome)
|
|
209
|
+
}
|
|
210
|
+
return Promise.resolve(scheduled).then(
|
|
211
|
+
settle((value) => value),
|
|
212
|
+
settle((error) => {
|
|
213
|
+
throw error
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The binding as \`installCurrentThreadHosts\` should see it: every export
|
|
220
|
+
* reads through, and \`registerTimerHost\` hands the addon a \`schedule\` that
|
|
221
|
+
* records its \`cancel\`. A binding without a callable \`registerTimerHost\` is
|
|
222
|
+
* passed through, so the package reports the mismatch as before.
|
|
223
|
+
*/
|
|
224
|
+
function __trackCurrentThreadHostTimers(binding) {
|
|
225
|
+
let registerTimerHost
|
|
226
|
+
try {
|
|
227
|
+
registerTimerHost = binding.registerTimerHost
|
|
228
|
+
} catch {
|
|
229
|
+
return binding
|
|
230
|
+
}
|
|
231
|
+
if (typeof registerTimerHost !== 'function') {
|
|
232
|
+
return binding
|
|
233
|
+
}
|
|
234
|
+
const view = Object.create(binding)
|
|
235
|
+
Object.defineProperty(view, 'registerTimerHost', {
|
|
236
|
+
configurable: true,
|
|
237
|
+
enumerable: true,
|
|
238
|
+
writable: true,
|
|
239
|
+
value: function (...args) {
|
|
240
|
+
const [, , schedule, cancel] = args
|
|
241
|
+
if (typeof schedule === 'function' && typeof cancel === 'function') {
|
|
242
|
+
args[2] = (id, ms) =>
|
|
243
|
+
__scheduleCurrentThreadHostTimer(schedule, cancel, id, ms)
|
|
244
|
+
}
|
|
245
|
+
return Reflect.apply(registerTimerHost, this, args)
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
return view
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clears every CurrentThread host timeout without entering wasm. Idempotent;
|
|
253
|
+
* a \`cancel\` that fails has already reported itself.
|
|
254
|
+
*/
|
|
255
|
+
function __releaseCurrentThreadHostTimers() {
|
|
256
|
+
__currentThreadHostTimersReleased = true
|
|
257
|
+
const releases = [...__currentThreadHostTimerCancels]
|
|
258
|
+
__currentThreadHostTimerCancels.clear()
|
|
259
|
+
for (const release of releases) {
|
|
260
|
+
try {
|
|
261
|
+
release()
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
`
|
|
266
|
+
: ''
|
|
125
267
|
const disposeCurrentThreadHosts = asyncRuntime
|
|
126
268
|
? ' __disposeCurrentThreadHosts()\n'
|
|
127
269
|
: ''
|
|
270
|
+
// After the in-flight check: a disposal that was already running when a
|
|
271
|
+
// thread died settles through the crash disposal itself, so every caller of
|
|
272
|
+
// it keeps getting the one promise it handed out.
|
|
273
|
+
const disposeAfterThreadCrash = threadCrashLatch
|
|
274
|
+
? ` if (!__wasiDisposed && __hasWasiThreadCrashed()) {
|
|
275
|
+
return __disposeWasiBindingAfterThreadCrash()
|
|
276
|
+
}
|
|
277
|
+
`
|
|
278
|
+
: ''
|
|
279
|
+
// A thread can die after the entry check above has passed. The disposal's
|
|
280
|
+
// polls and step boundaries check again, and a chain stopped that way — or
|
|
281
|
+
// failed any other way once a thread is dead — settles through the crash
|
|
282
|
+
// disposal. See `__abortWasiDisposalIfThreadCrashed`.
|
|
283
|
+
const abortDisposalAfterThreadCrash = (indent: string) =>
|
|
284
|
+
threadCrashLatch ? `${indent}__abortWasiDisposalIfThreadCrashed()\n` : ''
|
|
285
|
+
const settleDisposalAfterThreadCrash = (indent: string, exit: string) =>
|
|
286
|
+
threadCrashLatch
|
|
287
|
+
? `${indent}if (__settleWasiDisposalAfterThreadCrash(resolveDispose, rejectDispose)) {
|
|
288
|
+
${indent} ${exit}
|
|
289
|
+
${indent}}
|
|
290
|
+
`
|
|
291
|
+
: ''
|
|
292
|
+
// The barrier's poll ends in `…_finish`, which joins the runtime's work. A
|
|
293
|
+
// dead thread's work never goes idle, so after a crash the join would block
|
|
294
|
+
// this thread for good: leave the barrier parked and stop the disposal.
|
|
295
|
+
const finishWasmEnvCleanupUnlessCrashed = threadCrashLatch
|
|
296
|
+
? ` const finishCleanupUnlessCrashed = () => {
|
|
297
|
+
try {
|
|
298
|
+
__abortWasiDisposalIfThreadCrashed()
|
|
299
|
+
} catch (error) {
|
|
300
|
+
__finishParkedWasmEnvCleanup = undefined
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
finishCleanup()
|
|
304
|
+
}
|
|
305
|
+
`
|
|
306
|
+
: ''
|
|
307
|
+
const finishWasmEnvCleanupAfterPoll = threadCrashLatch
|
|
308
|
+
? 'finishCleanupUnlessCrashed'
|
|
309
|
+
: 'finishCleanup'
|
|
310
|
+
// The initialization rollback runs the same polls and steps. With the latch,
|
|
311
|
+
// its body keeps its own name and `__rollbackWasiInitialization` becomes the
|
|
312
|
+
// wrapper that stops it after a crash. See
|
|
313
|
+
// `__rollbackWasiInitializationAfterThreadCrash`.
|
|
314
|
+
const rollbackStepsName = threadCrashLatch
|
|
315
|
+
? '__runWasiInitializationRollbackSteps'
|
|
316
|
+
: '__rollbackWasiInitialization'
|
|
317
|
+
const rollbackAfterThreadCrash = threadCrashLatch
|
|
318
|
+
? `
|
|
319
|
+
/**
|
|
320
|
+
* The rollback above, stopped when a wasm thread has died.
|
|
321
|
+
*
|
|
322
|
+
* Its polls wait for work the dead thread still counts, so they never end, and
|
|
323
|
+
* the barrier's \`…_finish\` and \`Context.destroy()\` would re-enter wasm
|
|
324
|
+
* and wait on that thread for good. While the rollback runs, the poll turns
|
|
325
|
+
* and step boundaries stop it (\`__abortWasiDisposalIfThreadCrashed\`), and a
|
|
326
|
+
* crash seen before, during or after it ends it through
|
|
327
|
+
* \`__rollbackWasiInitializationAfterThreadCrash\`. Without a crash the
|
|
328
|
+
* result — synchronous or not — is passed through unchanged.
|
|
329
|
+
*/
|
|
330
|
+
function __rollbackWasiInitialization() {
|
|
331
|
+
if (__hasWasiThreadCrashed()) {
|
|
332
|
+
return __rollbackWasiInitializationAfterThreadCrash()
|
|
333
|
+
}
|
|
334
|
+
__wasiInitializationRollbackActive = true
|
|
335
|
+
let result
|
|
336
|
+
try {
|
|
337
|
+
result = __runWasiInitializationRollbackSteps()
|
|
338
|
+
} catch (error) {
|
|
339
|
+
__wasiInitializationRollbackActive = false
|
|
340
|
+
if (__hasWasiThreadCrashed()) {
|
|
341
|
+
return __rollbackWasiInitializationAfterThreadCrash()
|
|
342
|
+
}
|
|
343
|
+
throw error
|
|
344
|
+
}
|
|
345
|
+
if (!__isThenable(result)) {
|
|
346
|
+
__wasiInitializationRollbackActive = false
|
|
347
|
+
return __hasWasiThreadCrashed()
|
|
348
|
+
? __rollbackWasiInitializationAfterThreadCrash()
|
|
349
|
+
: result
|
|
350
|
+
}
|
|
351
|
+
return Promise.resolve(result).then(
|
|
352
|
+
(cleanupErrors) => {
|
|
353
|
+
__wasiInitializationRollbackActive = false
|
|
354
|
+
return __hasWasiThreadCrashed()
|
|
355
|
+
? __rollbackWasiInitializationAfterThreadCrash()
|
|
356
|
+
: cleanupErrors
|
|
357
|
+
},
|
|
358
|
+
(error) => {
|
|
359
|
+
__wasiInitializationRollbackActive = false
|
|
360
|
+
if (__hasWasiThreadCrashed()) {
|
|
361
|
+
return __rollbackWasiInitializationAfterThreadCrash()
|
|
362
|
+
}
|
|
363
|
+
throw error
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
`
|
|
368
|
+
: ''
|
|
128
369
|
|
|
129
370
|
return `
|
|
130
371
|
const __wasiDisposeSymbol = Symbol.for('${WASI_DISPOSE_SYMBOL}')
|
|
@@ -188,7 +429,7 @@ let __completeWasiDisposal = function () {}
|
|
|
188
429
|
// that stopped short of destroying the context. See
|
|
189
430
|
// \`__rollbackWasiInitialization\`.
|
|
190
431
|
let __retainWasiRollbackForRetry = function () {}
|
|
191
|
-
${currentThreadHosts}
|
|
432
|
+
${currentThreadHosts}${currentThreadHostTimersAfterThreadCrash}
|
|
192
433
|
function __isThenable(value) {
|
|
193
434
|
return (
|
|
194
435
|
value !== null &&
|
|
@@ -653,6 +894,7 @@ function __prepareWasmEnvCleanupWithTurns() {
|
|
|
653
894
|
// Publish the closer before yielding: from here until \`finishCleanup\` runs,
|
|
654
895
|
// a caller that cannot yield is entitled to end this handshake itself.
|
|
655
896
|
__finishParkedWasmEnvCleanup = finishCleanup
|
|
897
|
+
${finishWasmEnvCleanupUnlessCrashed}\
|
|
656
898
|
return (async () => {
|
|
657
899
|
// Unbounded, exactly like the async-work drain below. The wait ends when
|
|
658
900
|
// the addon reports its runtime work finished; the turns spent here are
|
|
@@ -660,6 +902,7 @@ function __prepareWasmEnvCleanupWithTurns() {
|
|
|
660
902
|
const pace = __createWasmRuntimePollPace()
|
|
661
903
|
for (;;) {
|
|
662
904
|
await __yieldWasmRuntimePollTurn(pace)
|
|
905
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
663
906
|
try {
|
|
664
907
|
if (!workPending()) {
|
|
665
908
|
return
|
|
@@ -670,7 +913,7 @@ function __prepareWasmEnvCleanupWithTurns() {
|
|
|
670
913
|
return
|
|
671
914
|
}
|
|
672
915
|
}
|
|
673
|
-
})().then(
|
|
916
|
+
})().then(${finishWasmEnvCleanupAfterPoll}, ${finishWasmEnvCleanupAfterPoll})
|
|
674
917
|
}
|
|
675
918
|
|
|
676
919
|
// Turns to wait for while the addon still reports queued settlements. Reaching
|
|
@@ -994,6 +1237,7 @@ function __drainWasiAsyncWork() {
|
|
|
994
1237
|
await new Promise((resolve) => {
|
|
995
1238
|
__scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS)
|
|
996
1239
|
})
|
|
1240
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
997
1241
|
}
|
|
998
1242
|
})(),
|
|
999
1243
|
).then(
|
|
@@ -1089,6 +1333,7 @@ function __finishWasiDisposal() {
|
|
|
1089
1333
|
}
|
|
1090
1334
|
|
|
1091
1335
|
function __continueWasiDisposal() {
|
|
1336
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1092
1337
|
const destroyResult = __destroyEmnapiContext()
|
|
1093
1338
|
if (__isThenable(destroyResult)) {
|
|
1094
1339
|
return Promise.resolve(destroyResult).then(__finishWasiDisposal)
|
|
@@ -1097,6 +1342,7 @@ function __continueWasiDisposal() {
|
|
|
1097
1342
|
}
|
|
1098
1343
|
|
|
1099
1344
|
function __drainWasmEnvForWasiDisposal() {
|
|
1345
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1100
1346
|
const drainResult = __drainWasmEnvCleanup()
|
|
1101
1347
|
if (__isThenable(drainResult)) {
|
|
1102
1348
|
return Promise.resolve(drainResult).then(__continueWasiDisposal)
|
|
@@ -1105,6 +1351,7 @@ function __drainWasmEnvForWasiDisposal() {
|
|
|
1105
1351
|
}
|
|
1106
1352
|
|
|
1107
1353
|
function __cleanUpWasmEnvForWasiDisposal() {
|
|
1354
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1108
1355
|
// Run the pre-teardown barrier — yielding the turns its two-phase form asks
|
|
1109
1356
|
// for, when the addon has one — then let the settlements it queued actually
|
|
1110
1357
|
// reach JavaScript, and only then destroy the environment. Doing any two of
|
|
@@ -1141,6 +1388,7 @@ function __disposeWasiBinding() {
|
|
|
1141
1388
|
if (__wasiDisposePromise) {
|
|
1142
1389
|
return __wasiDisposePromise
|
|
1143
1390
|
}
|
|
1391
|
+
${disposeAfterThreadCrash}\
|
|
1144
1392
|
if (__wasiDisposed) {
|
|
1145
1393
|
return Promise.resolve()
|
|
1146
1394
|
}
|
|
@@ -1157,6 +1405,7 @@ function __disposeWasiBinding() {
|
|
|
1157
1405
|
try {
|
|
1158
1406
|
result = __startWasiDisposal()
|
|
1159
1407
|
} catch (error) {
|
|
1408
|
+
${settleDisposalAfterThreadCrash(' ', 'return disposePromise')}\
|
|
1160
1409
|
__wasiDisposePromise = undefined
|
|
1161
1410
|
rejectDispose(error)
|
|
1162
1411
|
return disposePromise
|
|
@@ -1168,6 +1417,7 @@ function __disposeWasiBinding() {
|
|
|
1168
1417
|
resolveDispose(value)
|
|
1169
1418
|
},
|
|
1170
1419
|
(error) => {
|
|
1420
|
+
${settleDisposalAfterThreadCrash(' ', 'return')}\
|
|
1171
1421
|
__wasiDisposePromise = undefined
|
|
1172
1422
|
rejectDispose(error)
|
|
1173
1423
|
},
|
|
@@ -1203,6 +1453,7 @@ function __finishWasiInitializationRollback(cleanupErrors) {
|
|
|
1203
1453
|
}
|
|
1204
1454
|
|
|
1205
1455
|
function __destroyContextForWasiRollback(cleanupErrors) {
|
|
1456
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1206
1457
|
let destroyResult
|
|
1207
1458
|
try {
|
|
1208
1459
|
destroyResult = __destroyEmnapiContext()
|
|
@@ -1271,10 +1522,11 @@ function __retainFailedWasiRollback(cleanupErrors) {
|
|
|
1271
1522
|
* goes away. That is the deliberate choice: a hung promise is a silent liveness
|
|
1272
1523
|
* bug with no upper bound, while the retained bookkeeping is bounded by the page.
|
|
1273
1524
|
*/
|
|
1274
|
-
function
|
|
1525
|
+
function ${rollbackStepsName}() {
|
|
1275
1526
|
// The environment teardown this rollback performs, kept nested so it cannot
|
|
1276
1527
|
// be reached without the async-work drain below running first.
|
|
1277
1528
|
function __rollbackWasmEnvForWasiInitialization() {
|
|
1529
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1278
1530
|
const cleanupErrors = []
|
|
1279
1531
|
let prepareResult
|
|
1280
1532
|
try {
|
|
@@ -1300,6 +1552,7 @@ function __rollbackWasiInitialization() {
|
|
|
1300
1552
|
// did not finish never gets here: it retains instead, exactly as a drain that
|
|
1301
1553
|
// did not finish does.
|
|
1302
1554
|
function __drainWasmEnvForWasiRollback(cleanupErrors) {
|
|
1555
|
+
${abortDisposalAfterThreadCrash(' ')}\
|
|
1303
1556
|
let drainResult
|
|
1304
1557
|
try {
|
|
1305
1558
|
drainResult = __drainWasmEnvCleanup()
|
|
@@ -1346,6 +1599,7 @@ function __rollbackWasiInitialization() {
|
|
|
1346
1599
|
}
|
|
1347
1600
|
return __rollbackWasmEnvForWasiInitialization()
|
|
1348
1601
|
}
|
|
1602
|
+
${rollbackAfterThreadCrash}\
|
|
1349
1603
|
`
|
|
1350
1604
|
}
|
|
1351
1605
|
|
|
@@ -1499,10 +1753,35 @@ const __workerPoolSize = Math.max(
|
|
|
1499
1753
|
type: 'module',
|
|
1500
1754
|
})
|
|
1501
1755
|
__wasiWorkers.add(worker)
|
|
1756
|
+
__shareWasiAddonCrashFlag(worker)
|
|
1502
1757
|
${workerFsHandler}
|
|
1503
1758
|
${workerErrorHandler}
|
|
1504
1759
|
return worker
|
|
1505
1760
|
},
|
|
1761
|
+
`
|
|
1762
|
+
: ''
|
|
1763
|
+
// The pool is created before the wasm is instantiated, so the workers that
|
|
1764
|
+
// exist by `beforeInit` get the view there, after their 'load' message and
|
|
1765
|
+
// before any 'start'; a worker created later gets it first thing. A message
|
|
1766
|
+
// rather than worker options: a browser Worker has no `workerData`.
|
|
1767
|
+
const addonCrashFlagSharing = threads
|
|
1768
|
+
? `${ADDON_CRASH_FLAG_CAPTURE}
|
|
1769
|
+
function __shareWasiAddonCrashFlag(worker) {
|
|
1770
|
+
if (__wasiAddonCrashFlag === undefined) {
|
|
1771
|
+
return
|
|
1772
|
+
}
|
|
1773
|
+
try {
|
|
1774
|
+
worker.postMessage({ __napiRsAddonCrashFlag: __wasiAddonCrashFlag })
|
|
1775
|
+
} catch {}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
`
|
|
1779
|
+
: ''
|
|
1780
|
+
const captureAddonCrashFlag = threads
|
|
1781
|
+
? ` __captureWasiAddonCrashFlag(instance)
|
|
1782
|
+
for (const worker of __wasiWorkers) {
|
|
1783
|
+
__shareWasiAddonCrashFlag(worker)
|
|
1784
|
+
}
|
|
1506
1785
|
`
|
|
1507
1786
|
: ''
|
|
1508
1787
|
|
|
@@ -1542,6 +1821,7 @@ ${threads ? ' shared: true,\n' : ''}\
|
|
|
1542
1821
|
${workerPoolSizeBinding}\
|
|
1543
1822
|
let __emnapiContext
|
|
1544
1823
|
${createEmnapiContextLifecycle(asyncRuntime)}
|
|
1824
|
+
${addonCrashFlagSharing}\
|
|
1545
1825
|
let __wasiModule
|
|
1546
1826
|
let __napiModule
|
|
1547
1827
|
|
|
@@ -1575,6 +1855,7 @@ ${workerOption}\
|
|
|
1575
1855
|
},
|
|
1576
1856
|
beforeInit({ instance }) {
|
|
1577
1857
|
__napiInstance = instance
|
|
1858
|
+
${captureAddonCrashFlag}\
|
|
1578
1859
|
for (const name of Object.keys(instance.exports)) {
|
|
1579
1860
|
if (name.startsWith('__napi_register__')) {
|
|
1580
1861
|
instance.exports[name]()
|
|
@@ -3608,9 +3889,11 @@ export const createWasiBinding = (
|
|
|
3608
3889
|
} = require('@napi-rs/async-runtime')
|
|
3609
3890
|
`
|
|
3610
3891
|
: ''
|
|
3892
|
+
// The threaded loader has the thread crash latch, whose disposal releases
|
|
3893
|
+
// the host timers it tracks. See \`__trackCurrentThreadHostTimers\`.
|
|
3611
3894
|
const installAsyncRuntimeHosts = asyncRuntime
|
|
3612
3895
|
? ` __currentThreadHostsDisposer = __installCurrentThreadHosts(
|
|
3613
|
-
__napiModule.exports,
|
|
3896
|
+
${threads ? '__trackCurrentThreadHostTimers(__napiModule.exports)' : '__napiModule.exports'},
|
|
3614
3897
|
)
|
|
3615
3898
|
`
|
|
3616
3899
|
: ''
|
|
@@ -3694,7 +3977,13 @@ function __createWasiWorker(filename) {
|
|
|
3694
3977
|
return new Worker(filename, {
|
|
3695
3978
|
env: process.env,
|
|
3696
3979
|
execArgv: __workerExecArgv,
|
|
3697
|
-
workerData: {
|
|
3980
|
+
workerData: {
|
|
3981
|
+
hostRoot: __hostRoot,
|
|
3982
|
+
rootDir: __rootDir,
|
|
3983
|
+
crashFlag: __wasiThreadCrashFlag,
|
|
3984
|
+
crashReport: __wasiThreadCrashReport,
|
|
3985
|
+
addonCrashFlag: __wasiAddonCrashFlag,
|
|
3986
|
+
},
|
|
3698
3987
|
})
|
|
3699
3988
|
} catch (error) {
|
|
3700
3989
|
if (!error || error.code !== 'ERR_WORKER_INVALID_EXEC_ARGV') {
|
|
@@ -3711,6 +4000,505 @@ function __createWasiWorker(filename) {
|
|
|
3711
4000
|
}
|
|
3712
4001
|
`
|
|
3713
4002
|
: ''
|
|
4003
|
+
// The host timeouts a CurrentThread sleep armed, released without entering
|
|
4004
|
+
// wasm on every crash path. See `__trackCurrentThreadHostTimers`.
|
|
4005
|
+
const releaseCurrentThreadHostTimers = (indent: string) =>
|
|
4006
|
+
threads && asyncRuntime
|
|
4007
|
+
? `${indent}__releaseCurrentThreadHostTimers()\n`
|
|
4008
|
+
: ''
|
|
4009
|
+
// The threaded context defers emnapi's calls into wasm through
|
|
4010
|
+
// `__wasiSetImmediate`, which a crash disposal closes.
|
|
4011
|
+
const emnapiContextOptions = threads
|
|
4012
|
+
? '{ autoDestroy: false, features: { setImmediate: __wasiSetImmediate } }'
|
|
4013
|
+
: '{ autoDestroy: false }'
|
|
4014
|
+
// Only the threaded flavor has pool workers whose wasm thread can die under
|
|
4015
|
+
// this one. See `__disposeWasiBindingAtExit`.
|
|
4016
|
+
const threadCrashLatch = threads
|
|
4017
|
+
? `
|
|
4018
|
+
// Set to 1 by a pool worker (see wasi-worker.mjs) right before it reports that
|
|
4019
|
+
// its wasm thread died. Shared memory, so this thread reads it synchronously even
|
|
4020
|
+
// while the worker's 'error' event is still queued behind the code that is
|
|
4021
|
+
// exiting right now.
|
|
4022
|
+
const __wasiThreadCrashFlag = new Int32Array(new SharedArrayBuffer(4))
|
|
4023
|
+
// Written by the first pool worker whose wasm thread dies, before it raises the
|
|
4024
|
+
// flag: its error and \`threadId\` (layout in wasi-worker.mjs). Read without
|
|
4025
|
+
// waiting for any event, see \`__readWasiThreadCrashReport\`.
|
|
4026
|
+
const __wasiThreadCrashReport = new SharedArrayBuffer(4096)
|
|
4027
|
+
let __wasiThreadCrashReportRead
|
|
4028
|
+
// The first error a pool worker reported through its 'error' event, and that
|
|
4029
|
+
// worker's \`threadId\`. See \`__getWasiThreadCrashError\`.
|
|
4030
|
+
let __wasiThreadCrashWorkerError
|
|
4031
|
+
let __wasiThreadCrashWorkerId
|
|
4032
|
+
let __wasiThreadCrashError
|
|
4033
|
+
// Raised while \`__runWasiInitializationRollbackSteps\` runs, so its polls and
|
|
4034
|
+
// steps stop after a crash the way a public disposal's do.
|
|
4035
|
+
let __wasiInitializationRollbackActive = false
|
|
4036
|
+
let __wasiThreadCrashed = false
|
|
4037
|
+
|
|
4038
|
+
${ADDON_CRASH_FLAG_CAPTURE}
|
|
4039
|
+
/**
|
|
4040
|
+
* Whether any wasm thread of this binding has died: a trap or an uncaught error
|
|
4041
|
+
* in a pool worker, including one that failed to load after its thread spawn
|
|
4042
|
+
* had already been reported as started.
|
|
4043
|
+
*
|
|
4044
|
+
* After that the shared wasm state cannot be trusted: a lock the dead thread
|
|
4045
|
+
* held stays held, and a join or park that waits on it — the async runtime's
|
|
4046
|
+
* \`finish_shutdown\` waiting for the dead thread's work to go idle — blocks
|
|
4047
|
+
* this thread forever in a raw \`memory.atomic.wait32\`, which neither
|
|
4048
|
+
* emnapi's crash check nor a signal can interrupt.
|
|
4049
|
+
*/
|
|
4050
|
+
function __hasWasiThreadCrashed() {
|
|
4051
|
+
if (__wasiThreadCrashed || Atomics.load(__wasiThreadCrashFlag, 0) !== 0) {
|
|
4052
|
+
return true
|
|
4053
|
+
}
|
|
4054
|
+
const manager = __getWasiThreadManager()
|
|
4055
|
+
return Boolean(manager && manager._fatalError)
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
let __wasiThreadCrashDisposePromise
|
|
4059
|
+
// Raised by the crash disposal right before it terminates the workers. See
|
|
4060
|
+
// \`__wasiSetImmediate\`.
|
|
4061
|
+
let __wasiReentryClosed = false
|
|
4062
|
+
|
|
4063
|
+
/**
|
|
4064
|
+
* emnapi's \`features.setImmediate\` for this binding's context. emnapi defers
|
|
4065
|
+
* its calls back into wasm through it: \`_emnapi_set_immediate\` (libuv handle
|
|
4066
|
+
* closes, threadsafe-function finalizers), threadsafe-function dispatch
|
|
4067
|
+
* (\`async-send\`) and the finalizer queue. One queued before a crash disposal
|
|
4068
|
+
* terminates the workers still runs after it, and a worker terminated while it
|
|
4069
|
+
* held a lock in the wasm heap (napi's heap-sync allocator lock spins and never
|
|
4070
|
+
* gives up) leaves that call spinning on this thread for good. The binding is
|
|
4071
|
+
* unusable after a crash disposal, so those calls are dropped.
|
|
4072
|
+
*
|
|
4073
|
+
* Known residual, still able to enter wasm after a crash disposal because emnapi
|
|
4074
|
+
* offers no hook for them: the \`FinalizationRegistry\` callbacks that free
|
|
4075
|
+
* external memory when GC collects a value (\`_free\`, the shared-buffer meta
|
|
4076
|
+
* release), threadsafe-function dispatch of \`async-send\` type 1 and
|
|
4077
|
+
* \`_emnapi_next_tick\` (both \`Promise.resolve().then\`), and every deferred
|
|
4078
|
+
* call under emnapi 1.x, whose \`createContext\` ignores \`features\`.
|
|
4079
|
+
*/
|
|
4080
|
+
function __wasiSetImmediate(callback) {
|
|
4081
|
+
return setImmediate(function () {
|
|
4082
|
+
if (!__wasiReentryClosed) {
|
|
4083
|
+
callback()
|
|
4084
|
+
}
|
|
4085
|
+
})
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
/**
|
|
4089
|
+
* Stores the first error a pool worker reported, with the worker's id. Called
|
|
4090
|
+
* from the loader's own 'error' listener, which runs before emnapi's.
|
|
4091
|
+
*/
|
|
4092
|
+
function __recordWasiThreadCrashError(error, workerId) {
|
|
4093
|
+
if (__wasiThreadCrashWorkerError !== undefined || error === undefined) {
|
|
4094
|
+
return
|
|
4095
|
+
}
|
|
4096
|
+
__wasiThreadCrashWorkerError = error
|
|
4097
|
+
__wasiThreadCrashWorkerId = workerId
|
|
4098
|
+
__fillWasiThreadCrashError()
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
/**
|
|
4102
|
+
* The error in the shared crash report, rebuilt once it is complete: the
|
|
4103
|
+
* worker writes it before it raises the flag, so it is there as soon as the
|
|
4104
|
+
* crash is seen. Plain JavaScript over shared memory; never enters wasm.
|
|
4105
|
+
*/
|
|
4106
|
+
function __readWasiThreadCrashReport() {
|
|
4107
|
+
if (__wasiThreadCrashReportRead !== undefined) {
|
|
4108
|
+
return __wasiThreadCrashReportRead
|
|
4109
|
+
}
|
|
4110
|
+
try {
|
|
4111
|
+
const header = new Int32Array(__wasiThreadCrashReport, 0, 3)
|
|
4112
|
+
if (Atomics.load(header, 0) !== 2) {
|
|
4113
|
+
return
|
|
4114
|
+
}
|
|
4115
|
+
const length = Atomics.load(header, 1)
|
|
4116
|
+
const threadId = Atomics.load(header, 2)
|
|
4117
|
+
let error
|
|
4118
|
+
if (length > 0) {
|
|
4119
|
+
// Copied out of shared memory: TextDecoder does not take a shared view.
|
|
4120
|
+
const bytes = new Uint8Array(__wasiThreadCrashReport, 12, length).slice()
|
|
4121
|
+
const report = JSON.parse(new TextDecoder().decode(bytes))
|
|
4122
|
+
error = new Error(String(report.message))
|
|
4123
|
+
if (typeof report.name === 'string') {
|
|
4124
|
+
error.name = report.name
|
|
4125
|
+
}
|
|
4126
|
+
if (typeof report.stack === 'string') {
|
|
4127
|
+
error.stack = report.stack
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
__wasiThreadCrashReportRead = { error, threadId }
|
|
4131
|
+
} catch {
|
|
4132
|
+
__wasiThreadCrashReportRead = { error: undefined, threadId: 0 }
|
|
4133
|
+
}
|
|
4134
|
+
return __wasiThreadCrashReportRead
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
/**
|
|
4138
|
+
* The one error every crash path of this binding reports, created on first
|
|
4139
|
+
* use.
|
|
4140
|
+
*
|
|
4141
|
+
* The shared flag is raised before this thread has processed the worker's
|
|
4142
|
+
* 'error' event, and once the workers are terminated that event is often
|
|
4143
|
+
* never delivered, so neither it nor emnapi's \`_fatalError\` can be counted
|
|
4144
|
+
* on. The cause is filled in from the first source that has it: the error the
|
|
4145
|
+
* 'error' listener kept, else the worker's shared crash report, else
|
|
4146
|
+
* \`_fatalError\` — here, and again from the 'error' listener when it arrives
|
|
4147
|
+
* later. Until then the error carries only its message.
|
|
4148
|
+
*/
|
|
4149
|
+
function __getWasiThreadCrashError() {
|
|
4150
|
+
if (__wasiThreadCrashError === undefined) {
|
|
4151
|
+
__wasiThreadCrashError = new Error(
|
|
4152
|
+
'napi-rs: WASI binding cannot be disposed after a worker thread crashed',
|
|
4153
|
+
)
|
|
4154
|
+
}
|
|
4155
|
+
__fillWasiThreadCrashError()
|
|
4156
|
+
return __wasiThreadCrashError
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
function __fillWasiThreadCrashError() {
|
|
4160
|
+
const crashError = __wasiThreadCrashError
|
|
4161
|
+
if (crashError === undefined) {
|
|
4162
|
+
return
|
|
4163
|
+
}
|
|
4164
|
+
try {
|
|
4165
|
+
if (crashError.cause === undefined) {
|
|
4166
|
+
let cause = __wasiThreadCrashWorkerError
|
|
4167
|
+
if (cause === undefined) {
|
|
4168
|
+
const report = __readWasiThreadCrashReport()
|
|
4169
|
+
cause = report ? report.error : undefined
|
|
4170
|
+
}
|
|
4171
|
+
if (cause === undefined) {
|
|
4172
|
+
const manager = __getWasiThreadManager()
|
|
4173
|
+
cause = manager ? manager._fatalError : undefined
|
|
4174
|
+
}
|
|
4175
|
+
if (cause !== undefined && cause !== null) {
|
|
4176
|
+
crashError.cause = cause
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
if (crashError.workerThreadId === undefined) {
|
|
4180
|
+
let workerId = __wasiThreadCrashWorkerId
|
|
4181
|
+
if (workerId === undefined) {
|
|
4182
|
+
const report = __readWasiThreadCrashReport()
|
|
4183
|
+
workerId = report && report.threadId > 0 ? report.threadId : undefined
|
|
4184
|
+
}
|
|
4185
|
+
if (workerId !== undefined) {
|
|
4186
|
+
crashError.workerThreadId = workerId
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
} catch {}
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
/**
|
|
4193
|
+
* Let the event loop drain after a wasm thread died, without entering wasm.
|
|
4194
|
+
*
|
|
4195
|
+
* emnapi's \`Context\` keeps a \`NodejsWaitingRequestCounter\` on Node: a
|
|
4196
|
+
* \`MessagePort\` (\`refCounter.refHandle\`) that it refs when the count of
|
|
4197
|
+
* in-flight async work and threadsafe-function requests leaves zero and unrefs
|
|
4198
|
+
* when it comes back. The requests the dead thread held never complete, so the
|
|
4199
|
+
* count never returns to zero and the port holds the process open for good.
|
|
4200
|
+
* \`Context.destroy()\` would not release it either — and it runs cleanup
|
|
4201
|
+
* hooks, which is exactly what must not happen now. So unref the port directly.
|
|
4202
|
+
* The count is left as is: it stays above zero, so a later request never refs
|
|
4203
|
+
* the port again.
|
|
4204
|
+
*
|
|
4205
|
+
* The field is private in emnapi's typings, so read it defensively; a context
|
|
4206
|
+
* without it (a non-Node host, a future emnapi) is left alone.
|
|
4207
|
+
*/
|
|
4208
|
+
function __releaseEmnapiWaitingRequestHandle() {
|
|
4209
|
+
try {
|
|
4210
|
+
const refCounter = __emnapiContext && __emnapiContext.refCounter
|
|
4211
|
+
const refHandle = refCounter && refCounter.refHandle
|
|
4212
|
+
if (refHandle && typeof refHandle.unref === 'function') {
|
|
4213
|
+
refHandle.unref()
|
|
4214
|
+
}
|
|
4215
|
+
} catch {}
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
/**
|
|
4219
|
+
* The public disposer after a wasm thread died. The normal chain drains async
|
|
4220
|
+
* work, runs the environment cleanup barrier and destroys the context — every
|
|
4221
|
+
* one of those re-enters wasm, and the barrier's shutdown waits for the dead
|
|
4222
|
+
* thread's work in the same raw atomic wait \`__disposeWasiBindingAtExit\`
|
|
4223
|
+
* avoids. An app that handled the worker's error and then disposes would block
|
|
4224
|
+
* there for good. Only stop the workers, then reject: the binding was not
|
|
4225
|
+
* cleaned up and cannot be, so reporting success would be a lie. The context
|
|
4226
|
+
* is not destroyed — only its waiting-request port is unrefed, so the process
|
|
4227
|
+
* can exit on its own — and the 'exit' listener takes its short path too.
|
|
4228
|
+
*
|
|
4229
|
+
* Latched: every later call returns the same promise.
|
|
4230
|
+
*/
|
|
4231
|
+
function __disposeWasiBindingAfterThreadCrash() {
|
|
4232
|
+
if (__wasiThreadCrashDisposePromise) {
|
|
4233
|
+
return __wasiThreadCrashDisposePromise
|
|
4234
|
+
}
|
|
4235
|
+
__releaseEmnapiWaitingRequestHandle()
|
|
4236
|
+
${releaseCurrentThreadHostTimers(' ')}\
|
|
4237
|
+
// No call into wasm after this: see \`__wasiSetImmediate\`.
|
|
4238
|
+
__wasiReentryClosed = true
|
|
4239
|
+
let workerResult
|
|
4240
|
+
try {
|
|
4241
|
+
workerResult = __terminateWasiWorkers()
|
|
4242
|
+
} catch (terminateError) {
|
|
4243
|
+
workerResult = Promise.reject(terminateError)
|
|
4244
|
+
}
|
|
4245
|
+
// Built at settlement, after the workers stopped: by then the 'error' event
|
|
4246
|
+
// has usually delivered the worker's error.
|
|
4247
|
+
__wasiThreadCrashDisposePromise = Promise.resolve(workerResult).then(
|
|
4248
|
+
() => {
|
|
4249
|
+
throw __getWasiThreadCrashError()
|
|
4250
|
+
},
|
|
4251
|
+
(terminateError) => {
|
|
4252
|
+
throw __attachCleanupErrors(__getWasiThreadCrashError(), [
|
|
4253
|
+
terminateError,
|
|
4254
|
+
])
|
|
4255
|
+
},
|
|
4256
|
+
)
|
|
4257
|
+
return __wasiThreadCrashDisposePromise
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
/**
|
|
4261
|
+
* Stops a public disposal that a thread crash overtook.
|
|
4262
|
+
*
|
|
4263
|
+
* The check at the top of \`__disposeWasiBinding\` only sees a crash that came
|
|
4264
|
+
* first. A thread that dies once the chain is running leaves it polling for
|
|
4265
|
+
* work the dead thread still counts — the async-work drain for
|
|
4266
|
+
* \`napi_wasm_async_work_pending\`, the barrier's poll for
|
|
4267
|
+
* \`napi_wasm_runtime_work_pending\` — and neither count ever reaches zero, so
|
|
4268
|
+
* the poll's referenced timers keep the process alive forever with the
|
|
4269
|
+
* disposal promise pending. Each poll turn and each step boundary calls this,
|
|
4270
|
+
* and the throw ends the chain before anything re-enters wasm again; the
|
|
4271
|
+
* disposer then settles it through \`__settleWasiDisposalAfterThreadCrash\`.
|
|
4272
|
+
*
|
|
4273
|
+
* The initialization rollback runs the same polls and steps, so they stop while
|
|
4274
|
+
* it runs too; its wrapper, \`__rollbackWasiInitialization\`, then ends it
|
|
4275
|
+
* through \`__rollbackWasiInitializationAfterThreadCrash\`. A bare poll with
|
|
4276
|
+
* neither in flight is left alone.
|
|
4277
|
+
*/
|
|
4278
|
+
function __abortWasiDisposalIfThreadCrashed() {
|
|
4279
|
+
if (
|
|
4280
|
+
(__wasiDisposePromise !== undefined ||
|
|
4281
|
+
__wasiInitializationRollbackActive) &&
|
|
4282
|
+
__hasWasiThreadCrashed()
|
|
4283
|
+
) {
|
|
4284
|
+
throw new Error(
|
|
4285
|
+
'napi-rs: WASI disposal stopped because a worker thread crashed',
|
|
4286
|
+
)
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
/**
|
|
4291
|
+
* Settles an in-flight public disposal whose chain failed after a thread
|
|
4292
|
+
* died — stopped by \`__abortWasiDisposalIfThreadCrashed\` or failing any other
|
|
4293
|
+
* way — through the crash disposal: it releases the waiting-request port,
|
|
4294
|
+
* terminates the workers and rejects with the same error an entry-time crash
|
|
4295
|
+
* gets. \`__wasiDisposePromise\` stays set, so this caller, every caller that
|
|
4296
|
+
* joined it and every later one hold the same promise. Returns false when no
|
|
4297
|
+
* thread died, leaving the ordinary failure handling alone.
|
|
4298
|
+
*/
|
|
4299
|
+
function __settleWasiDisposalAfterThreadCrash(resolve, reject) {
|
|
4300
|
+
if (!__hasWasiThreadCrashed()) {
|
|
4301
|
+
return false
|
|
4302
|
+
}
|
|
4303
|
+
__disposeWasiBindingAfterThreadCrash().then(resolve, reject)
|
|
4304
|
+
return true
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
/**
|
|
4308
|
+
* Ends an initialization rollback after a wasm thread died, without entering
|
|
4309
|
+
* wasm: the barrier, its finish and \`Context.destroy()\` are skipped, and the
|
|
4310
|
+
* crash disposal releases the waiting-request port and terminates the workers,
|
|
4311
|
+
* once. The initialization error itself is left to propagate: the crash error
|
|
4312
|
+
* is returned as the rollback's cleanup error, so
|
|
4313
|
+
* \`__completeWasiInitializationRollback\` attaches it — as the cause when the
|
|
4314
|
+
* error has none — and keeps the record, since the context was not destroyed.
|
|
4315
|
+
*/
|
|
4316
|
+
function __rollbackWasiInitializationAfterThreadCrash() {
|
|
4317
|
+
const crashError = __getWasiThreadCrashError()
|
|
4318
|
+
// Its rejection is \`crashError\` itself, which the caller reports.
|
|
4319
|
+
void __disposeWasiBindingAfterThreadCrash().catch(() => {})
|
|
4320
|
+
return [crashError]
|
|
4321
|
+
}
|
|
4322
|
+
`
|
|
4323
|
+
: ''
|
|
4324
|
+
const skipTeardownAfterThreadCrash = threads
|
|
4325
|
+
? ` if (__hasWasiThreadCrashed()) {
|
|
4326
|
+
// Never re-enter wasm after a thread died: the environment cleanup joins
|
|
4327
|
+
// the dead thread's work and would hang the exit forever — SIGTERM
|
|
4328
|
+
// included, since its JavaScript listener never gets a turn. Stop the
|
|
4329
|
+
// workers and leave.
|
|
4330
|
+
${releaseCurrentThreadHostTimers(' ')}\
|
|
4331
|
+
try {
|
|
4332
|
+
void Promise.resolve(__terminateWasiWorkers()).catch(() => {})
|
|
4333
|
+
} catch {}
|
|
4334
|
+
return
|
|
4335
|
+
}
|
|
4336
|
+
`
|
|
4337
|
+
: ''
|
|
4338
|
+
// Threaded flavor only: keeps emnapi's idle reuse pool at the addon's
|
|
4339
|
+
// configured MultiThread worker count. See docs/wasi.md, "Thread pool
|
|
4340
|
+
// preload".
|
|
4341
|
+
const threadPoolReconcile = threads
|
|
4342
|
+
? `
|
|
4343
|
+
const __wasiThreadPoolReconcileSymbol = Symbol.for('${WASI_THREAD_POOL_RECONCILE_SYMBOL}')
|
|
4344
|
+
|
|
4345
|
+
/**
|
|
4346
|
+
* Takes a Worker out of emnapi's reuse pool, if it is still there. emnapi
|
|
4347
|
+
* terminates a pooled Worker that failed to load but (up to
|
|
4348
|
+
* @emnapi/wasi-threads 2.1.0) leaves it in the pool, where the next thread
|
|
4349
|
+
* spawn would pop it. A newer emnapi removes it itself, so this is a no-op
|
|
4350
|
+
* then.
|
|
4351
|
+
*/
|
|
4352
|
+
function __removeWasiPoolWorker(manager, worker) {
|
|
4353
|
+
const index = manager.unusedWorkers.indexOf(worker)
|
|
4354
|
+
if (index !== -1) {
|
|
4355
|
+
manager.unusedWorkers.splice(index, 1)
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
/**
|
|
4360
|
+
* Takes a Worker the thread manager just terminated out of \`__wasiWorkers\`
|
|
4361
|
+
* once it has exited, not before. \`terminateWorker\` only starts Node's
|
|
4362
|
+
* asynchronous \`worker.terminate()\` and drops its promise, so a disposal that
|
|
4363
|
+
* begins before the exit has to find this Worker in the set and wait for it
|
|
4364
|
+
* like any other. A second \`terminate()\` settles when the Worker has exited,
|
|
4365
|
+
* the way \`__terminateWasiWorkers\` waits.
|
|
4366
|
+
*/
|
|
4367
|
+
function __untrackWasiWorkerOnExit(worker) {
|
|
4368
|
+
const terminated = worker.terminate()
|
|
4369
|
+
if (__isThenable(terminated)) {
|
|
4370
|
+
Promise.resolve(terminated).then(
|
|
4371
|
+
() => {
|
|
4372
|
+
__wasiWorkers.delete(worker)
|
|
4373
|
+
},
|
|
4374
|
+
// Left tracked: disposal terminates it again and reports the error.
|
|
4375
|
+
() => {},
|
|
4376
|
+
)
|
|
4377
|
+
} else {
|
|
4378
|
+
__wasiWorkers.delete(worker)
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
/**
|
|
4383
|
+
* Matches emnapi's idle reuse pool to the addon's configured MultiThread worker
|
|
4384
|
+
* count, which the addon exports as \`napi_wasm_runtime_pool_workers\`
|
|
4385
|
+
* (napi-async-runtime; 0 under CurrentThread). \`reuseWorker: true\` starts the
|
|
4386
|
+
* pool empty, so without this every pool thread the first async call spawns
|
|
4387
|
+
* boots a Worker and loads the wasm into it first. Here each missing Worker is
|
|
4388
|
+
* created and starts loading now; a spawn later pops one that is already
|
|
4389
|
+
* booting. Idle Workers above the count are terminated, last in first out,
|
|
4390
|
+
* the way a spawn takes them. A Worker a spawn already took is not in the pool
|
|
4391
|
+
* and is left alone.
|
|
4392
|
+
*
|
|
4393
|
+
* Runs once after a successful load and, when the loader wraps it, after every
|
|
4394
|
+
* successful \`configureAsyncRuntime\`. Also reachable as
|
|
4395
|
+
* binding[Symbol.for('${WASI_THREAD_POOL_RECONCILE_SYMBOL}')]().
|
|
4396
|
+
* It never throws and never waits on a Worker: a Worker whose load fails is
|
|
4397
|
+
* dropped from the pool when its load rejects. Nothing at all happens after a
|
|
4398
|
+
* thread crash, once disposal started, or for an addon without the export.
|
|
4399
|
+
*/
|
|
4400
|
+
function __reconcileWasiThreadPool() {
|
|
4401
|
+
try {
|
|
4402
|
+
if (__wasiDisposed || __wasiDisposePromise || __hasWasiThreadCrashed()) {
|
|
4403
|
+
return
|
|
4404
|
+
}
|
|
4405
|
+
const read = __napiInstance?.exports?.napi_wasm_runtime_pool_workers
|
|
4406
|
+
if (typeof read !== 'function') {
|
|
4407
|
+
return
|
|
4408
|
+
}
|
|
4409
|
+
const count = read() >>> 0
|
|
4410
|
+
const manager = __getWasiThreadManager()
|
|
4411
|
+
if (
|
|
4412
|
+
!manager ||
|
|
4413
|
+
!Array.isArray(manager.unusedWorkers) ||
|
|
4414
|
+
typeof manager.allocateUnusedWorker !== 'function' ||
|
|
4415
|
+
typeof manager.loadWasmModuleToWorker !== 'function'
|
|
4416
|
+
) {
|
|
4417
|
+
return
|
|
4418
|
+
}
|
|
4419
|
+
// Both loops are bounded by the difference they start from, so a manager
|
|
4420
|
+
// that does not update \`unusedWorkers\` the way emnapi does cannot spin.
|
|
4421
|
+
for (let excess = manager.unusedWorkers.length - count; excess > 0; excess--) {
|
|
4422
|
+
const worker = manager.unusedWorkers[manager.unusedWorkers.length - 1]
|
|
4423
|
+
manager.terminateWorker(worker)
|
|
4424
|
+
__removeWasiPoolWorker(manager, worker)
|
|
4425
|
+
__untrackWasiWorkerOnExit(worker)
|
|
4426
|
+
// Compatibility with @emnapi/wasi-threads 2.1.0 and older:
|
|
4427
|
+
// \`terminateWorker\` installs a reporter that logs every emnapi message
|
|
4428
|
+
// still queued on the port, so a Worker that finished loading just
|
|
4429
|
+
// before it was terminated prints 'received "loaded" command from
|
|
4430
|
+
// terminated worker'. Nothing listens for that Worker any more, and a
|
|
4431
|
+
// newer emnapi ignores the late 'loaded' itself, so this is harmless
|
|
4432
|
+
// there. \`__terminateWasiWorkers\` does the same.
|
|
4433
|
+
worker.onmessage = undefined
|
|
4434
|
+
}
|
|
4435
|
+
for (let missing = count - manager.unusedWorkers.length; missing > 0; missing--) {
|
|
4436
|
+
let worker
|
|
4437
|
+
try {
|
|
4438
|
+
// Through \`onCreateWorker\`: tracked in \`__wasiWorkers\`, unref'd, and
|
|
4439
|
+
// handed the crash flags like any pool Worker.
|
|
4440
|
+
worker = manager.allocateUnusedWorker()
|
|
4441
|
+
manager
|
|
4442
|
+
.loadWasmModuleToWorker(worker)
|
|
4443
|
+
.then(undefined, () => __removeWasiPoolWorker(manager, worker))
|
|
4444
|
+
} catch {
|
|
4445
|
+
if (worker !== undefined) {
|
|
4446
|
+
__removeWasiPoolWorker(manager, worker)
|
|
4447
|
+
try {
|
|
4448
|
+
manager.terminateWorker(worker)
|
|
4449
|
+
__untrackWasiWorkerOnExit(worker)
|
|
4450
|
+
} catch {}
|
|
4451
|
+
}
|
|
4452
|
+
return
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
} catch {}
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
function __publishWasiThreadPoolReconcile(exports) {
|
|
4459
|
+
Object.defineProperty(exports, __wasiThreadPoolReconcileSymbol, {
|
|
4460
|
+
configurable: false,
|
|
4461
|
+
enumerable: false,
|
|
4462
|
+
value: __reconcileWasiThreadPool,
|
|
4463
|
+
writable: false,
|
|
4464
|
+
})
|
|
4465
|
+
}
|
|
4466
|
+
${
|
|
4467
|
+
asyncRuntime
|
|
4468
|
+
? `
|
|
4469
|
+
/**
|
|
4470
|
+
* Replaces the addon's \`configureAsyncRuntime\` export, if it has one, with a
|
|
4471
|
+
* wrapper that reconciles the pool after every successful call: a configure
|
|
4472
|
+
* changes the count the pool was preloaded for. A configure that throws changed
|
|
4473
|
+
* nothing, so its error propagates and the pool stays as it is. Matched by
|
|
4474
|
+
* name, like the CurrentThread host install: it is the export
|
|
4475
|
+
* napi-async-runtime's adapter defines, and an addon that defines its own under
|
|
4476
|
+
* that name gets the same reconcile. Never fails the load.
|
|
4477
|
+
*/
|
|
4478
|
+
function __wrapWasiConfigureAsyncRuntime(binding) {
|
|
4479
|
+
let configure
|
|
4480
|
+
try {
|
|
4481
|
+
configure = binding.configureAsyncRuntime
|
|
4482
|
+
} catch {
|
|
4483
|
+
return
|
|
4484
|
+
}
|
|
4485
|
+
if (typeof configure !== 'function') {
|
|
4486
|
+
return
|
|
4487
|
+
}
|
|
4488
|
+
try {
|
|
4489
|
+
binding.configureAsyncRuntime = function configureAsyncRuntime(...args) {
|
|
4490
|
+
const result = Reflect.apply(configure, this, args)
|
|
4491
|
+
try {
|
|
4492
|
+
__reconcileWasiThreadPool()
|
|
4493
|
+
} catch {}
|
|
4494
|
+
return result
|
|
4495
|
+
}
|
|
4496
|
+
} catch {}
|
|
4497
|
+
}
|
|
4498
|
+
`
|
|
4499
|
+
: ''
|
|
4500
|
+
}`
|
|
4501
|
+
: ''
|
|
3714
4502
|
const workerRuntimeImport = threads
|
|
3715
4503
|
? ` createOnMessage: __wasmCreateOnMessageForFsProxy,\n`
|
|
3716
4504
|
: ''
|
|
@@ -3755,10 +4543,18 @@ function __createWasiWorker(filename) {
|
|
|
3755
4543
|
// with threads there is no JavaScript seam at all, so `__drainWasiAsyncWork`
|
|
3756
4544
|
// asks the addon instead.
|
|
3757
4545
|
const emnapiPluginRequire = ` emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin,\n emnapiTSFNPlugin: __emnapiTSFNPlugin,\n`
|
|
4546
|
+
const captureAddonCrashFlag = threads
|
|
4547
|
+
? ' __captureWasiAddonCrashFlag(instance)\n'
|
|
4548
|
+
: ''
|
|
3758
4549
|
const workerOption = threads
|
|
3759
4550
|
? ` onCreateWorker() {
|
|
3760
4551
|
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
|
|
3761
4552
|
__wasiWorkers.add(worker)
|
|
4553
|
+
// Registered before emnapi's own listeners, which rethrow the error.
|
|
4554
|
+
worker.on('error', (error) => {
|
|
4555
|
+
__wasiThreadCrashed = true
|
|
4556
|
+
__recordWasiThreadCrashError(error, worker.threadId)
|
|
4557
|
+
})
|
|
3762
4558
|
worker.onmessage = ({ data }) => {
|
|
3763
4559
|
__wasmCreateOnMessageForFsProxy(__nodeFs)(data)
|
|
3764
4560
|
}
|
|
@@ -3813,6 +4609,8 @@ ${workerRuntimeImport}\
|
|
|
3813
4609
|
const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
|
|
3814
4610
|
${asyncRuntimeImport}\
|
|
3815
4611
|
${workerExecArgv}\
|
|
4612
|
+
${threadCrashLatch}\
|
|
4613
|
+
${threadPoolReconcile}\
|
|
3816
4614
|
|
|
3817
4615
|
const __cwd = process.cwd()
|
|
3818
4616
|
const __rootDir = __nodePath.parse(__cwd).root
|
|
@@ -3855,7 +4653,7 @@ if (__nodeFs.existsSync(__wasmDebugFilePath)) {
|
|
|
3855
4653
|
|
|
3856
4654
|
const __wasmFile = __nodeFs.readFileSync(__wasmFilePath)
|
|
3857
4655
|
let __emnapiContext
|
|
3858
|
-
${createEmnapiContextLifecycle(asyncRuntime)}
|
|
4656
|
+
${createEmnapiContextLifecycle(asyncRuntime, threads)}
|
|
3859
4657
|
const __wasiRollbackRegistrySymbol = Symbol.for('${WASI_ROLLBACK_REGISTRY_SYMBOL}')
|
|
3860
4658
|
const __wasiRollbackRegistryKey =
|
|
3861
4659
|
typeof __filename === 'string' ? __filename : __wasmFilePath
|
|
@@ -3959,6 +4757,7 @@ function __removeWasiExitListener() {
|
|
|
3959
4757
|
|
|
3960
4758
|
function __disposeWasiBindingAtExit() {
|
|
3961
4759
|
__wasiExitListenerRegistered = false
|
|
4760
|
+
${skipTeardownAfterThreadCrash}\
|
|
3962
4761
|
// An 'exit' handler cannot yield, so it cannot wait for queued promise
|
|
3963
4762
|
// settlements the way __startWasiDisposal does — the process is leaving and
|
|
3964
4763
|
// those promises have no observer left anyway. Run the synchronous teardown
|
|
@@ -4033,7 +4832,7 @@ try {
|
|
|
4033
4832
|
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
|
|
4034
4833
|
try {
|
|
4035
4834
|
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
|
|
4036
|
-
__emnapiCreateContext({
|
|
4835
|
+
__emnapiCreateContext(${emnapiContextOptions}),
|
|
4037
4836
|
__prepareWasmEnvCleanup,
|
|
4038
4837
|
__isPreparingWasmEnvCleanup,
|
|
4039
4838
|
)
|
|
@@ -4068,6 +4867,7 @@ ${workerOption}\
|
|
|
4068
4867
|
},
|
|
4069
4868
|
beforeInit({ instance }) {
|
|
4070
4869
|
__napiInstance = instance
|
|
4870
|
+
${captureAddonCrashFlag}\
|
|
4071
4871
|
for (const name of Object.keys(instance.exports)) {
|
|
4072
4872
|
if (name.startsWith('__napi_register__')) {
|
|
4073
4873
|
instance.exports[name]()
|
|
@@ -4076,7 +4876,9 @@ ${workerOption}\
|
|
|
4076
4876
|
},
|
|
4077
4877
|
}))
|
|
4078
4878
|
__publishWasiDispose(__napiModule.exports)
|
|
4879
|
+
${threads ? ' __publishWasiThreadPoolReconcile(__napiModule.exports)\n' : ''}\
|
|
4079
4880
|
${installAsyncRuntimeHosts}\
|
|
4881
|
+
${threads && asyncRuntime ? ' __wrapWasiConfigureAsyncRuntime(__napiModule.exports)\n' : ''}\
|
|
4080
4882
|
// The CommonJS tail below aliases \`__napiModule.exports\`; a named module
|
|
4081
4883
|
// export does not travel with it, so carry the marker on the binding itself
|
|
4082
4884
|
// too. Three things pin the stamp to exactly this spot:
|
|
@@ -4105,5 +4907,14 @@ ${installAsyncRuntimeHosts}\
|
|
|
4105
4907
|
__runWasiInitializationRollback(rollback)
|
|
4106
4908
|
throw rollback.error
|
|
4107
4909
|
}
|
|
4910
|
+
${
|
|
4911
|
+
threads
|
|
4912
|
+
? `// Preload the pool for the count the addon configured during registration.
|
|
4913
|
+
// See \`__reconcileWasiThreadPool\`.
|
|
4914
|
+
try {
|
|
4915
|
+
__reconcileWasiThreadPool()
|
|
4916
|
+
} catch {}
|
|
4108
4917
|
`
|
|
4918
|
+
: ''
|
|
4919
|
+
}`
|
|
4109
4920
|
}
|