signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
343 lines • 15.3 kB
JavaScript
;
/* 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