diff --git a/src/foreground-plugin.ts b/src/foreground-plugin.ts index 22f66e7..b734d21 100644 --- a/src/foreground-plugin.ts +++ b/src/foreground-plugin.ts @@ -1,5 +1,5 @@ import { CallContext, RESET, GET_BLOCK, BEGIN, END, ENV, STORE } from './call-context.ts'; -import { PluginOutput, type InternalConfig } from './interfaces.ts'; +import { PluginOutput, type InternalConfig, InternalWasi } from './interfaces.ts'; import { loadWasi } from './polyfills/deno-wasi.ts'; export const EXTISM_ENV = 'extism:host/env'; @@ -11,11 +11,13 @@ export class ForegroundPlugin { #modules: InstantiatedModule[]; #names: string[]; #active: boolean = false; + #wasi: InternalWasi | null; - constructor(context: CallContext, names: string[], modules: InstantiatedModule[]) { + constructor(context: CallContext, names: string[], modules: InstantiatedModule[], wasi: InternalWasi | null) { this.#context = context; this.#names = names; this.#modules = modules; + this.#wasi = wasi; } async reset(): Promise { @@ -144,7 +146,10 @@ export class ForegroundPlugin { } async close(): Promise { - // noop + if (this.#wasi) { + await this.#wasi.close(); + this.#wasi = null; + } } } @@ -191,5 +196,5 @@ export async function createForegroundPlugin( }), ); - return new ForegroundPlugin(context, names, instances); + return new ForegroundPlugin(context, names, instances, wasi); } diff --git a/src/interfaces.ts b/src/interfaces.ts index 7e4e61e..88d70c4 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -135,7 +135,11 @@ export interface ExtismPluginOptions { /** * Whether or not to run the Wasm module in a Worker thread. Requires * {@link Capabilities#hasWorkerCapability | `CAPABILITIES.hasWorkerCapability`} to - * be true. + * be true. Defaults to false. + * + * This feature is marked experimental as we work out [a bug](https://github.com/extism/js-sdk/issues/46). + * + * @experimental */ runInWorker?: boolean; @@ -190,6 +194,7 @@ export interface InternalConfig { export interface InternalWasi { importObject(): Promise>; initialize(instance: WebAssembly.Instance): Promise; + close(): Promise; } /** diff --git a/src/mod.ts b/src/mod.ts index 0da7a24..ace467b 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -77,7 +77,8 @@ export async function createPlugin( opts.config ??= {}; opts.fetch ??= fetch; - opts.runInWorker ??= CAPABILITIES.hasWorkerCapability; + // TODO(chrisdickinson): reset this to `CAPABILITIES.hasWorkerCapability` once we've fixed https://github.com/extism/js-sdk/issues/46. + opts.runInWorker ??= false; if (opts.runInWorker && !CAPABILITIES.hasWorkerCapability) { throw new Error( 'Cannot enable off-thread wasm; current context is not `crossOriginIsolated` (see https://mdn.io/crossOriginIsolated)', diff --git a/src/polyfills/browser-wasi.ts b/src/polyfills/browser-wasi.ts index 5de63e9..e2d4db3 100644 --- a/src/polyfills/browser-wasi.ts +++ b/src/polyfills/browser-wasi.ts @@ -49,6 +49,10 @@ export async function loadWasi( return context.wasiImport; }, + async close() { + // noop + }, + async initialize(instance: WebAssembly.Instance) { const memory = instance.exports.memory as WebAssembly.Memory; diff --git a/src/polyfills/deno-wasi.ts b/src/polyfills/deno-wasi.ts index 91fed35..c9e57c0 100644 --- a/src/polyfills/deno-wasi.ts +++ b/src/polyfills/deno-wasi.ts @@ -6,10 +6,10 @@ import { closeSync } from 'node:fs'; async function createDevNullFDs() { const [stdin, stdout] = await Promise.all([open(devNull, 'r'), open(devNull, 'w')]); - + let needsClose = true; const fr = new globalThis.FinalizationRegistry((held: number) => { try { - closeSync(held); + if (needsClose) closeSync(held); } catch { // The fd may already be closed. } @@ -17,14 +17,23 @@ async function createDevNullFDs() { fr.register(stdin, stdin.fd); fr.register(stdout, stdout.fd); - return [stdin.fd, stdout.fd, stdout.fd]; + return { + async close() { + needsClose = false; + await Promise.all([stdin.close(), stdout.close()]).catch(() => {}); + }, + fds: [stdin.fd, stdout.fd, stdout.fd], + }; } export async function loadWasi( allowedPaths: { [from: string]: string }, enableWasiOutput: boolean, ): Promise { - const [stdin, stdout, stderr] = enableWasiOutput ? [0, 1, 2] : await createDevNullFDs(); + const { + close, + fds: [stdin, stdout, stderr], + } = enableWasiOutput ? { async close() {}, fds: [0, 1, 2] } : await createDevNullFDs(); const context = new Context({ preopens: allowedPaths, exitOnReturn: false, @@ -38,6 +47,10 @@ export async function loadWasi( return context.exports; }, + async close() { + await close(); + }, + async initialize(instance: WebAssembly.Instance) { const memory = instance.exports.memory as WebAssembly.Memory; diff --git a/src/polyfills/node-wasi.ts b/src/polyfills/node-wasi.ts index 21833ff..512515a 100644 --- a/src/polyfills/node-wasi.ts +++ b/src/polyfills/node-wasi.ts @@ -6,25 +6,44 @@ import { closeSync } from 'node:fs'; async function createDevNullFDs() { const [stdin, stdout] = await Promise.all([open(devNull, 'r'), open(devNull, 'w')]); + let needsClose = true; + // TODO: make this check always run when bun fixes [1], so `fs.promises.open()` returns a `FileHandle` as expected. + // [1]: https://github.com/oven-sh/bun/issues/5918 + let close = async () => { + closeSync(stdin as any); + closeSync(stdout as any); + }; + if (typeof stdin !== 'number') { + const fr = new globalThis.FinalizationRegistry((held: number) => { + try { + if (needsClose) closeSync(held); + } catch { + // The fd may already be closed. + } + }); - const fr = new globalThis.FinalizationRegistry((held: number) => { - try { - closeSync(held); - } catch { - // The fd may already be closed. - } - }); - fr.register(stdin, stdin.fd); - fr.register(stdout, stdout.fd); + fr.register(stdin, stdin.fd); + fr.register(stdout, stdout.fd); + close = async () => { + needsClose = false; + await Promise.all([stdin.close(), stdout.close()]).catch(() => {}); + }; + } - return [stdin.fd, stdout.fd, stdout.fd]; + return { + close, + fds: [stdin.fd, stdout.fd, stdout.fd], + }; } export async function loadWasi( allowedPaths: { [from: string]: string }, enableWasiOutput: boolean, ): Promise { - const [stdin, stdout, stderr] = enableWasiOutput ? [0, 1, 2] : await createDevNullFDs(); + const { + close, + fds: [stdin, stdout, stderr], + } = enableWasiOutput ? { async close() {}, fds: [0, 1, 2] } : await createDevNullFDs(); const context = new WASI({ version: 'preview1', @@ -39,6 +58,10 @@ export async function loadWasi( return context.wasiImport; }, + async close() { + await close(); + }, + async initialize(instance: WebAssembly.Instance) { const memory = instance.exports.memory as WebAssembly.Memory;