diff --git a/src/background-plugin.ts b/src/background-plugin.ts index 9777b44..2e451ca 100644 --- a/src/background-plugin.ts +++ b/src/background-plugin.ts @@ -1,6 +1,7 @@ /*eslint-disable no-empty*/ import { CallContext, RESET, IMPORT_STATE, EXPORT_STATE, STORE, GET_BLOCK } from './call-context.ts'; import { PluginOutput, SAB_BASE_OFFSET, SharedArrayBufferSection, type InternalConfig } from './interfaces.ts'; +import { withTimeout } from './utils'; import { WORKER_URL } from './worker-url.ts'; import { Worker } from 'node:worker_threads'; import { CAPABILITIES } from './polyfills/deno-capabilities.ts'; @@ -143,7 +144,7 @@ class BackgroundPlugin { async call(funcName: string, input?: string | Uint8Array): Promise { const index = this.#context[STORE](input); - const [errorIdx, outputIdx] = await this.callBlock(funcName, index); + const [errorIdx, outputIdx] = await withTimeout(this.callBlock(funcName, index), this.opts.timeoutMs); const shouldThrow = errorIdx !== null; const idx = errorIdx ?? outputIdx; diff --git a/src/foreground-plugin.ts b/src/foreground-plugin.ts index b85229c..f71c5af 100644 --- a/src/foreground-plugin.ts +++ b/src/foreground-plugin.ts @@ -1,5 +1,6 @@ import { CallContext, RESET, GET_BLOCK, BEGIN, END, ENV, STORE } from './call-context.ts'; import { PluginOutput, type InternalConfig, InternalWasi } from './interfaces.ts'; +import { withTimeout } from './utils.ts'; import { loadWasi } from './polyfills/deno-wasi.ts'; export const EXTISM_ENV = 'extism:host/env'; @@ -11,11 +12,13 @@ export class ForegroundPlugin { #instancePair: InstantiatedModule; #active: boolean = false; #wasi: InternalWasi[]; + #opts: InternalConfig; - constructor(context: CallContext, instancePair: InstantiatedModule, wasi: InternalWasi[]) { + constructor(opts: InternalConfig, context: CallContext, instancePair: InstantiatedModule, wasi: InternalWasi[]) { this.#context = context; this.#instancePair = instancePair; this.#wasi = wasi; + this.#opts = opts; } async reset(): Promise { @@ -61,7 +64,8 @@ export class ForegroundPlugin { async call(funcName: string, input?: string | Uint8Array): Promise { const inputIdx = this.#context[STORE](input); - const [errorIdx, outputIdx] = await this.callBlock(funcName, inputIdx); + + const [errorIdx, outputIdx] = await withTimeout(this.callBlock(funcName, inputIdx), this.#opts.timeoutMs); const shouldThrow = errorIdx !== null; const idx = errorIdx ?? outputIdx; @@ -127,7 +131,7 @@ export async function createForegroundPlugin( const instance = await instantiateModule(['main'], modules[mainIndex], imports, opts, wasiList, names, modules, seen); - return new ForegroundPlugin(context, [modules[mainIndex], instance], wasiList); + return new ForegroundPlugin(opts, context, [modules[mainIndex], instance], wasiList); } async function instantiateModule( diff --git a/src/interfaces.ts b/src/interfaces.ts index 3b0352e..bff8c14 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -193,6 +193,13 @@ export interface ExtismPluginOptions { sharedArrayBufferSize?: number; } +export type ManifestOptions = { + allowedPaths?: { [key: string]: string } | undefined; + allowedHosts?: string[] | undefined; + config?: PluginConfigLike; + timeoutMs?: number | undefined; +}; + export interface InternalConfig { logger: Console; allowedHosts: string[]; @@ -203,6 +210,7 @@ export interface InternalConfig { wasiEnabled: boolean; config: PluginConfig; sharedArrayBufferSize: number; + timeoutMs: number; } export interface InternalWasi { @@ -307,6 +315,11 @@ export interface Manifest { * ``` */ allowed_hosts?: string[] | undefined; + + /** + * Plugin call timeout in milliseconds + */ + timeout_ms?: number | undefined; } /** diff --git a/src/manifest.ts b/src/manifest.ts index dbe243b..040085a 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -7,16 +7,11 @@ import type { ManifestWasmModule, ManifestLike, PluginConfigLike, + ManifestOptions, } from './interfaces.ts'; import { readFile } from './polyfills/node-fs.ts'; import { responseToModule } from './polyfills/response-to-module.ts'; -export type ManifestOptions = { - allowedPaths?: { [key: string]: string } | undefined; - allowedHosts?: string[] | undefined; - config?: PluginConfigLike; -}; - async function _populateWasmField(candidate: ManifestLike, _fetch: typeof fetch): Promise { if (candidate instanceof ArrayBuffer) { return { wasm: [{ data: new Uint8Array(candidate as ArrayBuffer) }] }; @@ -98,6 +93,7 @@ export async function toWasmModuleData( allowedPaths: manifest.allowed_paths, allowedHosts: manifest.allowed_hosts, config: manifest.config, + timeoutMs: manifest.timeout_ms, }; const manifestsWasm = await Promise.all( diff --git a/src/mod.ts b/src/mod.ts index 778a8d1..1bb9d4f 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -1,6 +1,6 @@ import { CAPABILITIES } from './polyfills/deno-capabilities.ts'; -import type { Manifest, ManifestLike, InternalConfig, ExtismPluginOptions, Plugin } from './interfaces.ts'; +import type { ManifestLike, InternalConfig, ExtismPluginOptions, Plugin } from './interfaces.ts'; import { toWasmModuleData as _toWasmModuleData } from './manifest.ts'; @@ -105,6 +105,7 @@ export async function createPlugin( config: opts.config, enableWasiOutput: opts.enableWasiOutput, sharedArrayBufferSize: Number(opts.sharedArrayBufferSize) || 1 << 16, + timeoutMs: manifestOpts.timeoutMs ?? Number.MAX_SAFE_INTEGER, }; return (opts.runInWorker ? _createBackgroundPlugin : _createForegroundPlugin)(ic, names, moduleData); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..70f5cc4 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,11 @@ +export function withTimeout(promise: Promise, ms: number): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Function call timed out')); + }, ms); + + promise.then(resolve, reject).finally(() => { + clearTimeout(timeout); + }); + }); +}