UNPKG

signalk-server

Version:

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

404 lines 17.5 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startWasmPlugin = startWasmPlugin; exports.stopWasmPlugin = stopWasmPlugin; exports.unloadWasmPlugin = unloadWasmPlugin; exports.reloadWasmPlugin = reloadWasmPlugin; exports.handleWasmPluginCrash = handleWasmPluginCrash; exports.shutdownAllWasmPlugins = shutdownAllWasmPlugins; /* eslint-disable @typescript-eslint/no-unused-vars */ /** * WASM Plugin Lifecycle Operations * * Manages plugin lifecycle operations including start, stop, reload, unload, * crash handling, and shutdown. Handles state transitions and cleanup. */ const debug_1 = __importDefault(require("debug")); const plugin_registry_1 = require("./plugin-registry"); const wasm_runtime_1 = require("../wasm-runtime"); const wasm_subscriptions_1 = require("../wasm-subscriptions"); const plugin_routes_1 = require("./plugin-routes"); 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 socket_manager_1 = require("../bindings/socket-manager"); const debug = (0, debug_1.default)('signalk:wasm:loader'); // Track poll timers for plugins that request periodic polling const pollTimers = new Map(); // Track delta subscription unsubscribe functions for plugins const deltaUnsubscribers = new Map(); // Mutex for serializing network-capable plugin starts // as-fetch uses global state that gets corrupted with parallel plugin starts let networkPluginStartMutex = Promise.resolve(); /** * Start a WASM plugin */ async function startWasmPlugin(app, pluginId) { const plugin = plugin_registry_1.wasmPlugins.get(pluginId); if (!plugin) { throw new Error(`WASM plugin ${pluginId} not found`); } if (plugin.status === 'running') { debug(`Plugin ${pluginId} already running`); return; } // Serialize starts for network-capable plugins to avoid as-fetch global state corruption if (plugin.metadata?.capabilities?.network) { debug(`Plugin ${pluginId} has network capability, waiting for mutex...`); const previousMutex = networkPluginStartMutex; let releaseMutex; networkPluginStartMutex = new Promise((resolve) => { releaseMutex = resolve; }); await previousMutex; debug(`Plugin ${pluginId} acquired start mutex`); try { await startWasmPluginInternal(app, plugin, pluginId); } finally { debug(`Plugin ${pluginId} releasing start mutex`); releaseMutex(); } } else { await startWasmPluginInternal(app, plugin, pluginId); } } async function startWasmPluginInternal(app, plugin, pluginId) { debug(`Starting WASM plugin: ${pluginId}`); (0, plugin_registry_1.setPluginStatus)(plugin, 'starting'); plugin.errorMessage = undefined; try { if (!plugin.instance) { throw new Error('Plugin instance not loaded'); } // Call plugin start() with configuration // Pass the entire configuration object including enableDebug at root level const startConfig = { ...plugin.configuration, enableDebug: plugin.enableDebug }; const configJson = JSON.stringify(startConfig); debug(`Starting plugin with config: ${configJson}`); const result = await plugin.instance.exports.start(configJson); if (result !== 0) { throw new Error(`Plugin start() returned error code: ${result}`); } // Update provider instance references after plugin_start() completes // Providers are registered during start(), so we need to update // references using BOTH the packageName (used in env bindings) and real pluginId if (plugin.packageName) { (0, resource_provider_1.updateResourceProviderInstance)(plugin.packageName, plugin.instance); (0, weather_provider_1.updateWeatherProviderInstance)(plugin.packageName, plugin.instance); (0, radar_provider_1.updateRadarProviderInstance)(plugin.packageName, plugin.instance); } (0, resource_provider_1.updateResourceProviderInstance)(pluginId, plugin.instance); (0, weather_provider_1.updateWeatherProviderInstance)(pluginId, plugin.instance); (0, radar_provider_1.updateRadarProviderInstance)(pluginId, plugin.instance); (0, plugin_registry_1.setPluginStatus)(plugin, 'running'); plugin.statusMessage = 'Running'; plugin.crashCount = 0; // Reset crash count on successful start plugin.restartBackoff = 1000; // Set up periodic polling for plugins that export poll() // This is a generic mechanism for plugins that need to poll hardware, // sockets, or external systems (e.g., radar, NMEA receivers, sensors) if (plugin.instance?.exports?.poll) { const pollInterval = 1000; // Poll every 1 second debug(`Setting up poll timer for ${pluginId} (${pollInterval}ms)`); const pollTimer = setInterval(() => { try { if (plugin.status === 'running' && plugin.instance?.exports?.poll) { const result = plugin.instance.exports.poll(); if (result !== 0) { debug(`[${pluginId}] poll() returned: ${result}`); } } } catch (pollError) { debug(`[${pluginId}] poll() error: ${pollError}`); } }, pollInterval); pollTimers.set(pluginId, pollTimer); } // Set up delta subscription if plugin exports delta_handler if (plugin.instance?.exports?.delta_handler) { debug(`Setting up delta subscription for ${pluginId}`); // Subscribe to deltas from the server if (app.signalk && typeof app.signalk.on === 'function') { const deltaHandler = (delta) => { try { if (plugin.status === 'running' && plugin.instance?.exports?.delta_handler) { const deltaJson = JSON.stringify(delta); plugin.instance.exports.delta_handler(deltaJson); } } catch (deltaError) { debug(`[${pluginId}] delta_handler error: ${deltaError}`); } }; // Subscribe to delta events app.signalk.on('delta', deltaHandler); // Store unsubscribe function deltaUnsubscribers.set(pluginId, () => { if (app.signalk && typeof app.signalk.removeListener === 'function') { app.signalk.removeListener('delta', deltaHandler); } }); debug(`Delta subscription active for ${pluginId}`); } else { debug(`Warning: app.signalk not available for delta subscription`); } } debug(`Successfully started WASM plugin: ${pluginId}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); (0, plugin_registry_1.setPluginStatus)(plugin, 'error'); plugin.errorMessage = errorMsg; debug(`Failed to start WASM plugin ${pluginId}: ${errorMsg}`); throw error; } } /** * Stop a WASM plugin */ async function stopWasmPlugin(pluginId) { const plugin = plugin_registry_1.wasmPlugins.get(pluginId); if (!plugin) { throw new Error(`WASM plugin ${pluginId} not found`); } debug(`Stopping WASM plugin: ${pluginId}`); try { // Cancel any pending restart timers const timer = plugin_registry_1.restartTimers.get(pluginId); if (timer) { clearTimeout(timer); plugin_registry_1.restartTimers.delete(pluginId); } // Cancel any poll timers const pollTimer = pollTimers.get(pluginId); if (pollTimer) { clearInterval(pollTimer); pollTimers.delete(pluginId); debug(`Stopped poll timer for ${pluginId}`); } // Unsubscribe from delta events const deltaUnsubscriber = deltaUnsubscribers.get(pluginId); if (deltaUnsubscriber) { deltaUnsubscriber(); deltaUnsubscribers.delete(pluginId); debug(`Stopped delta subscription for ${pluginId}`); } if (plugin.instance) { // Call plugin stop() const result = plugin.instance.exports.stop(); if (result !== 0) { debug(`Plugin stop() returned error code: ${result}`); } } // Clean up any sockets opened by this plugin socket_manager_1.socketManager.closeAllForPlugin(pluginId); (0, plugin_registry_1.setPluginStatus)(plugin, 'stopped'); plugin.statusMessage = 'Stopped'; debug(`Successfully stopped WASM plugin: ${pluginId}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); debug(`Error stopping WASM plugin ${pluginId}: ${errorMsg}`); (0, plugin_registry_1.setPluginStatus)(plugin, 'error'); plugin.errorMessage = errorMsg; throw error; } } /** * Unload a WASM plugin completely (remove from memory and unregister routes) */ async function unloadWasmPlugin(app, pluginId) { const plugin = plugin_registry_1.wasmPlugins.get(pluginId); if (!plugin) { throw new Error(`WASM plugin ${pluginId} not found`); } debug(`Unloading WASM plugin: ${pluginId}`); try { // Stop the plugin first if running if (plugin.status === 'running') { await stopWasmPlugin(pluginId); } // Remove HTTP routes from Express if (plugin.router) { debug(`Removing HTTP routes for ${pluginId}`); // Express doesn't have a built-in way to remove routes, so we need to // remove the middleware from the app stack const paths = (0, plugin_routes_1.backwardsCompat)(`/plugins/${pluginId}`); // Remove all route handlers for this plugin paths.forEach((path) => { if (app._router && app._router.stack) { app._router.stack = app._router.stack.filter((layer) => { // Remove layers that match this plugin's path if (layer.route) { const routePath = layer.route.path; // Handle both string and array cases for route.path if (typeof routePath === 'string') { return !routePath.startsWith(path); } else if (Array.isArray(routePath)) { return !routePath.some((p) => p.startsWith(path)); } return true; } if (layer.name === 'router' && layer.regexp) { return !layer.regexp.test(path); } return true; }); } }); plugin.router = undefined; debug(`Removed HTTP routes for ${pluginId}`); } // Destroy WASM instance and free memory if (plugin.instance) { debug(`Destroying WASM instance for ${pluginId}`); // Clear any references to help garbage collection plugin.instance = undefined; debug(`Destroyed WASM instance for ${pluginId}`); } (0, plugin_registry_1.setPluginStatus)(plugin, 'stopped'); plugin.statusMessage = 'Unloaded'; debug(`Successfully unloaded WASM plugin: ${pluginId}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); debug(`Error unloading WASM plugin ${pluginId}: ${errorMsg}`); (0, plugin_registry_1.setPluginStatus)(plugin, 'error'); plugin.errorMessage = errorMsg; throw error; } } /** * Reload a WASM plugin (hot-reload without server restart) */ async function reloadWasmPlugin(app, pluginId) { const plugin = plugin_registry_1.wasmPlugins.get(pluginId); if (!plugin) { throw new Error(`WASM plugin ${pluginId} not found`); } debug(`Reloading WASM plugin: ${pluginId}`); try { const wasRunning = plugin.status === 'running'; // Stop the plugin if (wasRunning) { await stopWasmPlugin(pluginId); } // Save current configuration const savedConfig = plugin.configuration; // Reload WASM module const runtime = (0, wasm_runtime_1.getWasmRuntime)(); await runtime.reloadPlugin(pluginId); // Get new instance const newInstance = runtime.getInstance(pluginId); if (!newInstance) { throw new Error('Failed to get reloaded instance'); } plugin.instance = newInstance; // Update schema from new instance const schemaJson = newInstance.exports.schema(); plugin.schema = schemaJson ? JSON.parse(schemaJson) : {}; // Restart if it was running if (wasRunning) { await startWasmPlugin(app, pluginId); } plugin.statusMessage = 'Reloaded successfully'; debug(`Successfully reloaded WASM plugin: ${pluginId}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); (0, plugin_registry_1.setPluginStatus)(plugin, 'error'); plugin.errorMessage = `Reload failed: ${errorMsg}`; debug(`Failed to reload WASM plugin ${pluginId}: ${errorMsg}`); throw error; } } /** * Handle WASM plugin crash with automatic restart */ async function handleWasmPluginCrash(app, pluginId, error) { const plugin = plugin_registry_1.wasmPlugins.get(pluginId); if (!plugin) { return; } plugin.crashCount++; plugin.lastCrash = new Date(); (0, plugin_registry_1.setPluginStatus)(plugin, 'crashed'); plugin.errorMessage = `Crashed: ${error.message}`; debug(`WASM plugin ${pluginId} crashed (count: ${plugin.crashCount}): ${error.message}`); // Give up after 3 crashes in quick succession if (plugin.crashCount >= 3) { (0, plugin_registry_1.setPluginStatus)(plugin, 'error'); plugin.errorMessage = 'Plugin repeatedly crashing, automatic restart disabled'; debug(`Plugin ${pluginId} disabled after 3 crashes`); return; } // Schedule restart with exponential backoff plugin.restartBackoff = Math.min(plugin.restartBackoff * 2, 30000); // Max 30 seconds debug(`Scheduling restart for ${pluginId} in ${plugin.restartBackoff}ms`); const timer = setTimeout(async () => { try { debug(`Attempting automatic restart of ${pluginId}`); await reloadWasmPlugin(app, pluginId); plugin.statusMessage = 'Recovered from crash'; } catch (restartError) { debug(`Failed to restart ${pluginId}:`, restartError); (0, plugin_registry_1.setPluginStatus)(plugin, 'error'); plugin.errorMessage = 'Failed to recover from crash'; } }, plugin.restartBackoff); plugin_registry_1.restartTimers.set(pluginId, timer); } /** * Shutdown all WASM plugins */ async function shutdownAllWasmPlugins() { debug('Shutting down all WASM plugins'); debug(`Number of plugins in registry: ${plugin_registry_1.wasmPlugins.size}`); // Clear all restart timers for (const timer of plugin_registry_1.restartTimers.values()) { clearTimeout(timer); } plugin_registry_1.restartTimers.clear(); // Stop all plugins const plugins = Array.from(plugin_registry_1.wasmPlugins.values()); debug(`Plugins to shutdown: ${plugins.map((p) => `${p.id}(${p.status})`).join(', ')}`); for (const plugin of plugins) { try { if (plugin.status === 'running') { debug(`Stopping plugin ${plugin.id}...`); await stopWasmPlugin(plugin.id); debug(`Plugin ${plugin.id} stopped, status now: ${plugin.status}`); } else { debug(`Plugin ${plugin.id} not running (status=${plugin.status}), skipping stop`); } } catch (error) { debug(`Error stopping plugin ${plugin.id}:`, error); } } // Shutdown runtime const runtime = (0, wasm_runtime_1.getWasmRuntime)(); await runtime.shutdown(); // Reset singletons (0, wasm_runtime_1.resetWasmRuntime)(); (0, wasm_subscriptions_1.resetSubscriptionManager)(); plugin_registry_1.wasmPlugins.clear(); debug('All WASM plugins shut down'); } //# sourceMappingURL=plugin-lifecycle.js.map