UNPKG

signalk-server

Version:

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

343 lines 15.3 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-require-imports */ /** * WASM Plugin Registration and Management * * Manages the global plugin registry and handles plugin registration. * Maintains the plugin map and provides lookup functions. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.restartTimers = exports.wasmPlugins = void 0; exports.initializeLifecycleFunctions = initializeLifecycleFunctions; exports.setPluginStatus = setPluginStatus; exports.registerWasmPlugin = registerWasmPlugin; exports.getAllWasmPlugins = getAllWasmPlugins; exports.getWasmPlugin = getWasmPlugin; const path = __importStar(require("path")); const fs = __importStar(require("fs")); const debug_1 = __importDefault(require("debug")); const express_1 = __importDefault(require("express")); const wasm_runtime_1 = require("../wasm-runtime"); const wasm_storage_1 = require("../wasm-storage"); 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 pluginid_1 = require("../../pluginid"); const debug = (0, debug_1.default)('signalk:wasm:loader'); // Global plugin registry exports.wasmPlugins = new Map(); // Crash recovery timers exports.restartTimers = new Map(); // Forward declarations for circular dependency resolution let _startWasmPlugin; let _updateWasmPluginConfig; let _unloadWasmPlugin; let _stopWasmPlugin; /** * Initialize lifecycle function references (called from index.ts to resolve circular dependencies) */ function initializeLifecycleFunctions(startWasmPlugin, updateWasmPluginConfig, unloadWasmPlugin, stopWasmPlugin) { _startWasmPlugin = startWasmPlugin; _updateWasmPluginConfig = updateWasmPluginConfig; _unloadWasmPlugin = unloadWasmPlugin; _stopWasmPlugin = stopWasmPlugin; } /** * Helper to update plugin status and sync state property */ function setPluginStatus(plugin, status) { plugin.status = status; plugin.state = status; } /** * Add Node.js plugin compatibility properties to WASM plugin * This allows WASM plugins to be used interchangeably with Node.js plugins */ function addNodejsPluginCompat(plugin, pluginId) { // Add 'started' getter for Node.js plugin compatibility Object.defineProperty(plugin, 'started', { get() { return this.status === 'running'; }, enumerable: true, configurable: true }); plugin.stop = async function () { if (_stopWasmPlugin) { await _stopWasmPlugin(pluginId); } }; } /** * Mount webapp static files and register with app.webapps for WASM plugins * that have the signalk-webapp keyword */ function mountWasmWebapp(app, packageName, packageJson, location) { const keywords = packageJson.keywords || []; // Check if this is a webapp if (!keywords.includes('signalk-webapp')) { return; } // Find public folder const packagePath = path.join(location, packageName); let webappPath = packagePath; if (fs.existsSync(path.join(packagePath, 'public'))) { webappPath = path.join(packagePath, 'public'); } // Mount static files debug(`Mounting WASM webapp /${packageName}: ${webappPath}`); app.use('/' + packageName, express_1.default.static(webappPath)); // Create webapp metadata for admin UI const webappMetadata = { name: packageName, version: packageJson.version, description: packageJson.description || '', keywords: keywords, signalk: packageJson.signalk || {} }; // Register with app.webapps if (!app.webapps) { app.webapps = []; } // Avoid duplicates if (!app.webapps.find((w) => w.name === packageName)) { app.webapps.push(webappMetadata); debug(`Registered WASM webapp: ${packageName}`); } // Also register as embeddable webapp if it has that keyword if (keywords.includes('signalk-embeddable-webapp')) { if (!app.embeddablewebapps) { app.embeddablewebapps = []; } if (!app.embeddablewebapps.find((w) => w.name === packageName)) { app.embeddablewebapps.push(webappMetadata); debug(`Registered WASM embeddable webapp: ${packageName}`); } } } /** * Register a WASM plugin from package metadata */ async function registerWasmPlugin(app, packageName, metadata, location, configPath) { debug(`Registering WASM plugin: ${packageName} from ${location}`); try { // Read package.json to get WASM metadata const packageJson = require(path.join(location, packageName, 'package.json')); if (!packageJson.wasmManifest) { throw new Error('Missing wasmManifest in package.json'); } const wasmPath = path.join(location, packageName, packageJson.wasmManifest); // Mount webapp static files if this is a signalk-webapp mountWasmWebapp(app, packageName, packageJson, location); const capabilities = { network: packageJson.wasmCapabilities?.network || false, storage: packageJson.wasmCapabilities?.storage || 'vfs-only', dataRead: packageJson.wasmCapabilities?.dataRead !== false, // default true dataWrite: packageJson.wasmCapabilities?.dataWrite !== false, // default true serialPorts: packageJson.wasmCapabilities?.serialPorts || false, putHandlers: packageJson.wasmCapabilities?.putHandlers || false, httpEndpoints: packageJson.wasmCapabilities?.httpEndpoints || false, resourceProvider: packageJson.wasmCapabilities?.resourceProvider || false, weatherProvider: packageJson.wasmCapabilities?.weatherProvider || false, radarProvider: packageJson.wasmCapabilities?.radarProvider || false, rawSockets: packageJson.wasmCapabilities?.rawSockets || false }; // Load WASM module temporarily to extract schema and display name const tempVfsRoot = path.join(configPath, 'plugin-config-data', '.temp-' + (0, pluginid_1.derivePluginId)(packageName)); if (!fs.existsSync(tempVfsRoot)) { fs.mkdirSync(tempVfsRoot, { recursive: true }); } const runtime = (0, wasm_runtime_1.getWasmRuntime)(); const tempInstance = await runtime.loadPlugin(packageName, wasmPath, tempVfsRoot, capabilities, app); // Derive plugin ID from npm package name (not from WASM exports) // This ensures uniqueness via npm registry and prevents ID conflicts const pluginId = (0, pluginid_1.derivePluginId)(packageName); // Plugin display name from WASM exports, fallback to package name const pluginName = tempInstance.exports.name?.() || packageName; const schemaJson = tempInstance.exports.schema(); const schema = schemaJson ? JSON.parse(schemaJson) : {}; // Update the plugin instance reference for providers (0, resource_provider_1.updateResourceProviderInstance)(pluginId, tempInstance); (0, weather_provider_1.updateWeatherProviderInstance)(pluginId, tempInstance); (0, radar_provider_1.updateRadarProviderInstance)(pluginId, tempInstance); // Now check config using the REAL plugin ID const storagePaths = (0, wasm_storage_1.getPluginStoragePaths)(configPath, pluginId, packageName); const savedConfig = (0, wasm_storage_1.readPluginConfig)(storagePaths.configFile); // If plugin is disabled, create minimal plugin object and return early if (savedConfig.enabled === false) { debug(`Plugin ${packageName} is disabled, schema already extracted from WASM`); // Do NOT write config file here - UI shows "Configure" button when no config file exists // Config file will be created when user actually configures the plugin // Create a minimal plugin object without keeping WASM loaded const plugin = { id: pluginId, name: pluginName, type: 'wasm', packageName, version: metadata.version || packageJson.version, enabled: false, enableDebug: savedConfig.enableDebug || false, keywords: packageJson.keywords || [], packageLocation: location, configPath, metadata: { id: pluginId, name: pluginName, packageName, version: metadata.version || packageJson.version, wasmManifest: packageJson.wasmManifest, capabilities, packageLocation: location }, instance: null, // Instance was destroyed status: 'stopped', schema, // Schema extracted from temp load configuration: savedConfig.configuration, // Keep undefined/null for UI "Configure" button logic crashCount: 0, restartBackoff: 1000, description: packageJson.description || '', state: 'stopped', format: tempInstance.format // Preserve format from temp instance }; // Add Node.js plugin compatibility properties addNodejsPluginCompat(plugin, pluginId); // Register minimal plugin in global map exports.wasmPlugins.set(pluginId, plugin); // Add to app.plugins array if (app.plugins) { app.plugins.push(plugin); } // Add to app.pluginsMap if (app.pluginsMap) { app.pluginsMap[pluginId] = plugin; } // Set up basic REST API routes even though plugin is disabled // This allows Plugin Config UI to read/write config and enable the plugin (0, plugin_routes_1.setupWasmPluginRoutes)(app, plugin, configPath, _updateWasmPluginConfig, _startWasmPlugin, _unloadWasmPlugin, _stopWasmPlugin); debug(`Registered disabled WASM plugin: ${pluginId} (${pluginName}) - schema available, instance not loaded`); return plugin; } // Plugin is enabled - proceed with full load debug(`Plugin ${packageName} is enabled, initializing VFS and preparing for startup`); // Initialize VFS with the real plugin ID (0, wasm_storage_1.initializePluginVfs)(storagePaths); // Clean up temp VFS if (fs.existsSync(tempVfsRoot)) { fs.rmSync(tempVfsRoot, { recursive: true, force: true }); } // Write initial config file if it doesn't exist if (!fs.existsSync(storagePaths.configFile)) { debug(`Creating initial config file for ${packageName}`); (0, wasm_storage_1.writePluginConfig)(storagePaths.configFile, savedConfig); } // Use the instance we already loaded const instance = tempInstance; // Create plugin object const plugin = { id: pluginId, name: pluginName, type: 'wasm', packageName, version: metadata.version || packageJson.version, enabled: savedConfig.enabled || false, enableDebug: savedConfig.enableDebug || false, keywords: packageJson.keywords || [], packageLocation: location, configPath, metadata: { id: pluginId, name: pluginName, packageName, version: metadata.version || packageJson.version, wasmManifest: packageJson.wasmManifest, capabilities, packageLocation: location }, instance, status: 'stopped', schema, configuration: savedConfig.configuration, // Keep undefined/null for UI "Configure" button logic crashCount: 0, restartBackoff: 1000, // Start with 1 second description: packageJson.description || '', state: 'stopped', format: instance.format // WASM binary format (wasi-p1 or component-model) }; // Add Node.js plugin compatibility properties addNodejsPluginCompat(plugin, pluginId); // Register in global map exports.wasmPlugins.set(pluginId, plugin); // Add to app.plugins array for unified plugin management if (app.plugins) { app.plugins.push(plugin); } // Add to app.pluginsMap for plugin API compatibility if (app.pluginsMap) { app.pluginsMap[pluginId] = plugin; } // Set up REST API routes for this plugin (0, plugin_routes_1.setupWasmPluginRoutes)(app, plugin, configPath, _updateWasmPluginConfig, _startWasmPlugin, _unloadWasmPlugin, _stopWasmPlugin); debug(`Registered WASM plugin: ${pluginId} (${pluginName})`); // Auto-start if enabled if (plugin.enabled) { await _startWasmPlugin(app, pluginId); } return plugin; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); debug(`Failed to register WASM plugin ${packageName}: ${errorMsg}`); throw new Error(`Failed to register WASM plugin: ${errorMsg}`); } } /** * Get all WASM plugins */ function getAllWasmPlugins() { return Array.from(exports.wasmPlugins.values()); } /** * Get a WASM plugin by ID */ function getWasmPlugin(pluginId) { return exports.wasmPlugins.get(pluginId); } //# sourceMappingURL=plugin-registry.js.map