UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

449 lines 20 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-unused-vars */ /** * Standard WASM Loader * * Loads WASI P1 plugins (AssemblyScript and Rust library plugins) */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadStandardPlugin = loadStandardPlugin; const debug_1 = __importDefault(require("debug")); const loader_1 = __importDefault(require("@assemblyscript/loader")); const env_imports_1 = require("../bindings/env-imports"); const resource_provider_1 = require("../bindings/resource-provider"); const weather_provider_1 = require("../bindings/weather-provider"); const radar_provider_1 = require("../bindings/radar-provider"); const fetch_wrapper_1 = require("../utils/fetch-wrapper"); const debug = (0, debug_1.default)('signalk:wasm:loader:standard'); // Use Node.js built-in WASI (Node 20+ required) const { WASI } = require('node:wasi'); /** * Load a standard WASI P1 plugin (AssemblyScript or Rust library) */ async function loadStandardPlugin(pluginId, wasmPath, wasmBuffer, vfsRoot, capabilities, app) { debug(`Loading standard WASM plugin: ${pluginId} from ${wasmPath}`); // Create WASI instance with VFS isolation debug(`Creating WASI instance for ${pluginId}`); const wasi = new WASI({ version: 'preview1', env: { PLUGIN_ID: pluginId }, args: [], preopens: { '/': vfsRoot } }); debug(`WASI instance created`); // Compile WASM module debug(`Compiling WASM module for inspection...`); let module; try { module = await WebAssembly.compile(wasmBuffer); debug(`WASM module compiled successfully`); } catch (compileError) { debug(`WASM compilation failed: ${compileError}`); throw compileError; } // Inspect module to determine plugin type const imports = WebAssembly.Module.imports(module); const moduleExports = WebAssembly.Module.exports(module); debug(`Module has ${imports.length} imports, ${moduleExports.length} exports`); debug.enabled && debug(`Module imports: ${JSON.stringify(imports.map((i) => `${i.module}.${i.name}`).slice(0, 20))}`); // Detect plugin type // Note: plugin_id is optional since ID can be derived from package.json name const hasPluginId = moduleExports.some((e) => e.name === 'plugin_id'); const hasPluginName = moduleExports.some((e) => e.name === 'plugin_name'); const hasPluginStart = moduleExports.some((e) => e.name === 'plugin_start'); const hasAllocate = moduleExports.some((e) => e.name === 'allocate'); const hasStart = moduleExports.some((e) => e.name === '_start'); const isRustLibraryPlugin = hasPluginId && hasAllocate; const isRustPlugin = hasStart; // AssemblyScript plugins must have plugin_name and plugin_start (plugin_id is optional) const isAssemblyScriptPlugin = (hasPluginId || (hasPluginName && hasPluginStart)) && !hasAllocate && !hasStart; debug(`Plugin type detection: AS=${isAssemblyScriptPlugin}, RustLib=${isRustLibraryPlugin}, RustCmd=${isRustPlugin}`); // Get WASI imports const wasiImports = (wasi.getImportObject ? wasi.getImportObject() : wasi.getImports(module)); debug(`Got WASI imports`); // Refs that will be populated after instantiation const memoryRef = { current: null }; const rawExportsRef = { current: null }; const asLoaderRef = { current: null }; // Create environment imports const envImports = (0, env_imports_1.createEnvImports)({ pluginId, capabilities, app, memoryRef, rawExports: rawExportsRef, asLoaderInstance: asLoaderRef }); // Initialize as-fetch handler for network capability let fetchHandler = null; let fetchImports = {}; if (capabilities.network) { debug(`Setting up as-fetch handler for network capability`); const nodeFetch = (0, fetch_wrapper_1.getNodeFetch)(); // Create a wrapper that reads strings from WASM memory const fetchWrapper = async (urlPtr, init) => { let url; if (typeof urlPtr === 'number') { if (!memoryRef.current) { throw new Error('WASM memory not available for string conversion'); } // Read AssemblyScript string from memory (UTF-16LE) const SIZE_OFFSET = -4; const memView = new Uint32Array(memoryRef.current.buffer); const strLengthInBytes = memView[(urlPtr + SIZE_OFFSET) >>> 2]; const strLengthInChars = strLengthInBytes >>> 1; const strView = new Uint16Array(memoryRef.current.buffer, urlPtr, strLengthInChars); url = String.fromCharCode(...Array.from(strView)); debug(`Converted WASM string pointer ${urlPtr} to URL: ${url}`); } else { url = String(urlPtr); } return nodeFetch(url, init); }; // Dynamic import for ESM-only as-fetch package const { FetchHandler } = await import('as-fetch/bindings.raw.esm.js'); fetchHandler = new FetchHandler(fetchWrapper); fetchImports = fetchHandler.imports; } // Instantiate the module let instance; let asLoaderInstance = null; let rawExports; if (isAssemblyScriptPlugin) { debug(`Using AssemblyScript loader for ${pluginId}`); asLoaderInstance = await loader_1.default.instantiate(module, { wasi_snapshot_preview1: wasiImports.wasi_snapshot_preview1 || wasiImports, env: envImports, ...fetchImports }); instance = asLoaderInstance.instance; rawExports = asLoaderInstance.exports; asLoaderRef.current = asLoaderInstance; debug(`AssemblyScript instance created with loader`); } else { // Standard WebAssembly instantiation for Rust plugins instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasiImports.wasi_snapshot_preview1 || wasiImports, env: envImports, ...fetchImports }); rawExports = instance.exports; debug(`Standard WASM instance created`); } // Set refs for use in callbacks rawExportsRef.current = rawExports; if (rawExports.memory) { memoryRef.current = rawExports.memory; } // Store reference for Asyncify resume let asyncifyResumeFunction = null; // NOTE: Do NOT initialize as-fetch handler here! // as-fetch uses global state that gets corrupted if multiple plugins are loaded in parallel. // The handler is initialized right before plugin_start() is called, protected by a mutex. // Initialize based on plugin type if (isRustPlugin) { debug(`Initializing Rust command plugin: ${pluginId}`); wasi.start(instance); } else if (isRustLibraryPlugin) { debug(`Initialized Rust library plugin: ${pluginId}`); // Initialize WASI runtime without calling _start (for library plugins) // This sets up fd_write and other syscalls properly if (typeof wasi.initialize === 'function') { debug(`Calling wasi.initialize() for Rust library plugin`); wasi.initialize(instance); } // Also call _initialize if present (Rust static constructors) if (rawExports._initialize) { debug(`Calling _initialize for Rust library plugin`); rawExports._initialize(); } } else if (isAssemblyScriptPlugin) { debug(`Initialized AssemblyScript plugin: ${pluginId}`); } else { throw new Error(`Unknown WASM plugin format for ${pluginId}`); } // Create normalized export interface const exports = createPluginExports(isAssemblyScriptPlugin, isRustLibraryPlugin, asLoaderInstance, rawExports, () => asyncifyResumeFunction, (fn) => { asyncifyResumeFunction = fn; }, fetchHandler, capabilities); // Create setter for asyncify resume that can be used by external callers const setAsyncifyResume = (fn) => { asyncifyResumeFunction = fn; }; const pluginInstance = { pluginId, wasmPath, vfsRoot, capabilities, format: 'wasi-p1', wasi, module, instance, exports, asLoader: asLoaderInstance, setAsyncifyResume }; // Update provider references (0, resource_provider_1.updateResourceProviderInstance)(pluginId, pluginInstance); (0, weather_provider_1.updateWeatherProviderInstance)(pluginId, pluginInstance); (0, radar_provider_1.updateRadarProviderInstance)(pluginId, pluginInstance); debug(`Successfully loaded WASM plugin: ${pluginId}`); return pluginInstance; } /** * Create normalized plugin exports based on plugin type */ function createPluginExports(isAssemblyScriptPlugin, isRustLibraryPlugin, asLoaderInstance, rawExports, getAsyncifyResume, setAsyncifyResume, fetchHandler, capabilities) { let idFunc; let nameFunc; let schemaFunc; let startFunc; let stopFunc; if (isAssemblyScriptPlugin && asLoaderInstance) { idFunc = () => { const ptr = asLoaderInstance.exports.plugin_id(); return asLoaderInstance.exports.__getString(ptr); }; nameFunc = () => { const ptr = asLoaderInstance.exports.plugin_name(); return asLoaderInstance.exports.__getString(ptr); }; schemaFunc = () => { const ptr = asLoaderInstance.exports.plugin_schema(); return asLoaderInstance.exports.__getString(ptr); }; startFunc = async (config) => { debug(`Calling plugin_start with config: ${config.substring(0, 100)}...`); // Re-initialize as-fetch handler to refresh ASYNCIFY_MEM view // This is needed because memory may have grown since init(), detaching the old buffer view if (fetchHandler && capabilities.network) { debug(`Re-initializing as-fetch handler before plugin_start`); fetchHandler.init(rawExports, () => { debug(`FetchHandler calling main function to resume execution`); const resumeFn = getAsyncifyResume(); if (resumeFn) { resumeFn(); } }); } const encoder = new TextEncoder(); const configBytes = encoder.encode(config); const configLen = configBytes.length; const configPtr = asLoaderInstance.exports.__new(configLen, 0); const memory = asLoaderInstance.exports.memory.buffer; const memoryView = new Uint8Array(memory); memoryView.set(configBytes, configPtr); let resumePromiseResolve = null; const resumePromise = new Promise((resolve) => { resumePromiseResolve = resolve; }); setAsyncifyResume(() => { debug(`Re-calling plugin_start to resume from rewind state`); // Check Asyncify state - as-fetch calls asyncify_start_rewind() before calling us // State 0 = normal (rewind already completed), skip to avoid double-rewind // State 1 = unwound (shouldn't happen, as-fetch would have started rewind) // State 2 = rewinding (expected, proceed with resume) if (typeof asLoaderInstance.exports.asyncify_get_state === 'function') { const currentState = asLoaderInstance.exports.asyncify_get_state(); if (currentState === 0) { debug(`Plugin in normal state (state=0), rewind already completed, skipping`); return; } debug(`Asyncify state before resume: ${currentState}`); } try { // Re-read memory buffer in case it was detached during async operation const currentMemory = asLoaderInstance.exports.memory.buffer; debug(`Memory buffer size: ${currentMemory.byteLength}, configPtr: ${configPtr}, configLen: ${configLen}`); const resumeResult = asLoaderInstance.exports.plugin_start(configPtr, configLen); if (resumePromiseResolve) { resumePromiseResolve(); } return resumeResult; } catch (err) { debug(`Error during Asyncify rewind: ${err.message}`); if (resumePromiseResolve) { resumePromiseResolve(); } throw err; } }); const result = asLoaderInstance.exports.plugin_start(configPtr, configLen); if (typeof asLoaderInstance.exports.asyncify_get_state === 'function') { const state = asLoaderInstance.exports.asyncify_get_state(); debug(`Asyncify state after plugin_start: ${state}`); if (state === 1) { debug(`Plugin is in unwound state - waiting for async operation to complete`); await resumePromise; debug(`Async operation completed, plugin execution resumed`); } else { setAsyncifyResume(null); } } if (typeof asLoaderInstance.exports.__free === 'function') { asLoaderInstance.exports.__free(configPtr); } return result; }; stopFunc = () => asLoaderInstance.exports.plugin_stop(); } else if (isRustLibraryPlugin) { debug(`Setting up Rust library plugin exports with buffer-based strings`); const callRustStringFunc = (funcName) => { const func = rawExports[funcName]; if (typeof func !== 'function') { debug(`Warning: ${funcName} not found in exports`); return ''; } const maxLen = 8192; const allocate = rawExports.allocate; if (typeof allocate !== 'function') { throw new Error('Rust plugin missing allocate export'); } const outPtr = allocate(maxLen); if (!outPtr) { throw new Error(`Failed to allocate ${maxLen} bytes for ${funcName}`); } try { const writtenLen = func(outPtr, maxLen); if (writtenLen <= 0) { debug(`${funcName} returned ${writtenLen}`); return ''; } const memory = rawExports.memory; const bytes = new Uint8Array(memory.buffer, outPtr, writtenLen); const decoder = new TextDecoder('utf-8'); const result = decoder.decode(bytes); debug(`${funcName} returned: ${result.substring(0, 100)}...`); return result; } finally { const deallocate = rawExports.deallocate; if (typeof deallocate === 'function') { deallocate(outPtr, maxLen); } } }; idFunc = () => callRustStringFunc('plugin_id'); nameFunc = () => callRustStringFunc('plugin_name'); schemaFunc = () => callRustStringFunc('plugin_schema'); startFunc = (config) => { debug(`Calling Rust plugin_start with config: ${config.substring(0, 100)}...`); const encoder = new TextEncoder(); const configBytes = encoder.encode(config); const configLen = configBytes.length; const allocate = rawExports.allocate; const configPtr = allocate(configLen); const memory = rawExports.memory; const memoryView = new Uint8Array(memory.buffer); memoryView.set(configBytes, configPtr); try { const result = rawExports.plugin_start(configPtr, configLen); debug(`plugin_start returned: ${result}`); return result; } finally { const deallocate = rawExports.deallocate; if (typeof deallocate === 'function') { deallocate(configPtr, configLen); } } }; stopFunc = () => { const result = rawExports.plugin_stop(); debug(`plugin_stop returned: ${result}`); return result; }; } else { // Rust command plugins or unknown idFunc = rawExports.id; nameFunc = rawExports.name; schemaFunc = rawExports.schema; startFunc = rawExports.start; stopFunc = rawExports.stop; } // Wrap http_endpoints if it exists const httpEndpointsFunc = rawExports.http_endpoints ? isAssemblyScriptPlugin && asLoaderInstance ? () => { const ptr = asLoaderInstance.exports.http_endpoints(); return asLoaderInstance.exports.__getString(ptr); } : rawExports.http_endpoints : undefined; // Wrap poll if it exists (for plugins that need periodic execution) const pollFunc = rawExports.poll ? isAssemblyScriptPlugin && asLoaderInstance ? () => asLoaderInstance.exports.poll() : rawExports.poll : undefined; // Wrap delta_handler if it exists (for plugins that subscribe to deltas) let deltaHandlerFunc = undefined; if (rawExports.delta_handler) { if (isAssemblyScriptPlugin && asLoaderInstance) { deltaHandlerFunc = (deltaJson) => { // Pass delta JSON string to the WASM delta_handler const ptr = asLoaderInstance.exports.__newString(deltaJson); asLoaderInstance.exports.delta_handler(ptr); }; } else if (isRustLibraryPlugin) { // Rust library plugin: buffer-based string passing deltaHandlerFunc = (deltaJson) => { const encoder = new TextEncoder(); const deltaBytes = encoder.encode(deltaJson); const deltaLen = deltaBytes.length; const allocate = rawExports.allocate; if (typeof allocate !== 'function') { debug('Rust plugin missing allocate export for delta_handler'); return; } const deltaPtr = allocate(deltaLen); const memory = rawExports.memory; const memoryView = new Uint8Array(memory.buffer); memoryView.set(deltaBytes, deltaPtr); try { rawExports.delta_handler(deltaPtr, deltaLen); } finally { const deallocate = rawExports.deallocate; if (typeof deallocate === 'function') { deallocate(deltaPtr, deltaLen); } } }; } else { deltaHandlerFunc = rawExports.delta_handler; } } return { id: idFunc, name: nameFunc, schema: schemaFunc, start: startFunc, stop: stopFunc, memory: rawExports.memory, ...(httpEndpointsFunc && { http_endpoints: httpEndpointsFunc }), ...(pollFunc && { poll: pollFunc }), ...(deltaHandlerFunc && { delta_handler: deltaHandlerFunc }) }; } //# sourceMappingURL=standard-loader.js.map