UNPKG

signalk-server

Version:

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

699 lines 25.9 kB
"use strict"; /* eslint-disable @typescript-eslint/no-unused-vars */ /** * WASM Socket Manager * * Manages UDP and TCP sockets for WASM plugins that need raw network access * (e.g., radar plugins, NMEA receivers, etc.) * * Uses Node.js dgram and net modules, bridged to WASM via FFI */ 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.tcpSocketManager = exports.socketManager = void 0; const dgram = __importStar(require("dgram")); const net = __importStar(require("net")); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)('signalk:wasm:sockets'); /** * Socket Manager - singleton for managing plugin sockets */ class SocketManager { sockets = new Map(); nextSocketId = 1; /** * Create a new UDP socket * @param pluginId - Plugin that owns the socket * @param type - Socket type: 'udp4' or 'udp6' * @returns Socket ID, or -1 on error */ createSocket(pluginId, type = 'udp4') { try { const socketId = this.nextSocketId++; const socket = dgram.createSocket({ type, reuseAddr: true // Allow multiple plugins to bind to same port }); const managed = { socket, pluginId, bound: false, bindPromise: null, receiveBuffer: [], maxBufferSize: 1000, // Max buffered datagrams multicastGroups: new Set(), pendingOptions: [] }; // Set up message handler to buffer incoming data socket.on('message', (msg, rinfo) => { if (managed.receiveBuffer.length >= managed.maxBufferSize) { // Drop oldest message if buffer full managed.receiveBuffer.shift(); } managed.receiveBuffer.push({ data: Buffer.from(msg), // Copy the buffer address: rinfo.address, port: rinfo.port, timestamp: Date.now() }); }); socket.on('error', (err) => { debug(`[${pluginId}] Socket ${socketId} error: ${err.message}`); }); socket.on('close', () => { debug(`[${pluginId}] Socket ${socketId} closed`); this.sockets.delete(socketId); }); this.sockets.set(socketId, managed); debug(`[${pluginId}] Created socket ${socketId} (${type})`); return socketId; } catch (error) { debug(`Failed to create socket: ${error}`); return -1; } } /** * Bind socket to a port * @param socketId - Socket to bind * @param port - Port number (0 for any available port) * @param address - Address to bind to (optional, defaults to all interfaces) * @returns 0 on success, -1 on error */ bind(socketId, port, address) { const managed = this.sockets.get(socketId); if (!managed) { debug(`Socket ${socketId} not found`); return Promise.resolve(-1); } // Store the promise so setBroadcast etc. can wait for it managed.bindPromise = new Promise((resolve) => { try { managed.socket.bind(port, address, () => { managed.bound = true; const addr = managed.socket.address(); debug(`[${managed.pluginId}] Socket ${socketId} bound to ${addr.address}:${addr.port}`); // Apply any pending socket options now that we're bound for (const option of managed.pendingOptions) { try { if (option.type === 'broadcast') { managed.socket.setBroadcast(option.value); debug(`[${managed.pluginId}] Applied deferred setBroadcast(${option.value})`); } else if (option.type === 'multicastTTL') { managed.socket.setMulticastTTL(option.value); debug(`[${managed.pluginId}] Applied deferred setMulticastTTL(${option.value})`); } else if (option.type === 'multicastLoopback') { managed.socket.setMulticastLoopback(option.value); debug(`[${managed.pluginId}] Applied deferred setMulticastLoopback(${option.value})`); } else if (option.type === 'joinMulticast') { const { multicastAddress, interfaceAddress } = option.value; if (interfaceAddress) { managed.socket.addMembership(multicastAddress, interfaceAddress); } else { managed.socket.addMembership(multicastAddress); } managed.multicastGroups.add(multicastAddress); debug(`[${managed.pluginId}] Applied deferred joinMulticast(${multicastAddress})`); } else if (option.type === 'leaveMulticast') { const { multicastAddress, interfaceAddress } = option.value; if (interfaceAddress) { managed.socket.dropMembership(multicastAddress, interfaceAddress); } else { managed.socket.dropMembership(multicastAddress); } managed.multicastGroups.delete(multicastAddress); debug(`[${managed.pluginId}] Applied deferred leaveMulticast(${multicastAddress})`); } } catch (optionError) { debug(`[${managed.pluginId}] Error applying deferred option ${option.type}: ${optionError}`); } } managed.pendingOptions = []; resolve(0); }); } catch (error) { debug(`[${managed.pluginId}] Bind error: ${error}`); resolve(-1); } }); return managed.bindPromise; } /** * Join a multicast group * @param socketId - Socket to use * @param multicastAddress - Multicast group address (e.g., "239.254.2.0") * @param interfaceAddress - Interface address to use (optional) * @returns 0 on success, -1 on error */ joinMulticast(socketId, multicastAddress, interfaceAddress) { const managed = this.sockets.get(socketId); if (!managed) { debug(`Socket ${socketId} not found`); return -1; } // If socket is not yet bound, defer the multicast join until bind completes if (!managed.bound) { debug(`[${managed.pluginId}] Deferring joinMulticast(${multicastAddress}) until socket is bound`); managed.pendingOptions.push({ type: 'joinMulticast', value: { multicastAddress, interfaceAddress } }); return 0; } try { if (interfaceAddress) { managed.socket.addMembership(multicastAddress, interfaceAddress); } else { managed.socket.addMembership(multicastAddress); } managed.multicastGroups.add(multicastAddress); debug(`[${managed.pluginId}] Socket ${socketId} joined multicast ${multicastAddress}`); return 0; } catch (error) { debug(`[${managed.pluginId}] Join multicast error: ${error}`); return -1; } } /** * Leave a multicast group * @param socketId - Socket to use * @param multicastAddress - Multicast group address * @param interfaceAddress - Interface address (optional) * @returns 0 on success, -1 on error */ leaveMulticast(socketId, multicastAddress, interfaceAddress) { const managed = this.sockets.get(socketId); if (!managed) { debug(`Socket ${socketId} not found`); return -1; } // If socket is not yet bound, defer the multicast leave until bind completes if (!managed.bound) { debug(`[${managed.pluginId}] Deferring leaveMulticast(${multicastAddress}) until socket is bound`); managed.pendingOptions.push({ type: 'leaveMulticast', value: { multicastAddress, interfaceAddress } }); return 0; } try { if (interfaceAddress) { managed.socket.dropMembership(multicastAddress, interfaceAddress); } else { managed.socket.dropMembership(multicastAddress); } managed.multicastGroups.delete(multicastAddress); debug(`[${managed.pluginId}] Socket ${socketId} left multicast ${multicastAddress}`); return 0; } catch (error) { debug(`[${managed.pluginId}] Leave multicast error: ${error}`); return -1; } } /** * Set socket options */ setMulticastTTL(socketId, ttl) { const managed = this.sockets.get(socketId); if (!managed) return -1; // If socket is not yet bound, defer the option if (!managed.bound) { debug(`[${managed.pluginId}] Deferring setMulticastTTL(${ttl}) until socket is bound`); managed.pendingOptions.push({ type: 'multicastTTL', value: ttl }); return 0; } try { managed.socket.setMulticastTTL(ttl); return 0; } catch (error) { debug(`[${managed.pluginId}] setMulticastTTL error: ${error}`); return -1; } } setMulticastLoopback(socketId, enabled) { const managed = this.sockets.get(socketId); if (!managed) return -1; // If socket is not yet bound, defer the option if (!managed.bound) { debug(`[${managed.pluginId}] Deferring setMulticastLoopback(${enabled}) until socket is bound`); managed.pendingOptions.push({ type: 'multicastLoopback', value: enabled }); return 0; } try { managed.socket.setMulticastLoopback(enabled); return 0; } catch (error) { debug(`[${managed.pluginId}] setMulticastLoopback error: ${error}`); return -1; } } setBroadcast(socketId, enabled) { const managed = this.sockets.get(socketId); if (!managed) return -1; // If socket is not yet bound, defer the option if (!managed.bound) { debug(`[${managed.pluginId}] Deferring setBroadcast(${enabled}) until socket is bound`); managed.pendingOptions.push({ type: 'broadcast', value: enabled }); return 0; } try { managed.socket.setBroadcast(enabled); return 0; } catch (error) { debug(`[${managed.pluginId}] setBroadcast error: ${error}`); return -1; } } /** * Send data via UDP * @param socketId - Socket to use * @param data - Data to send * @param address - Destination address * @param port - Destination port * @returns Bytes sent, or -1 on error */ send(socketId, data, address, port) { return new Promise((resolve) => { const managed = this.sockets.get(socketId); if (!managed) { debug(`Socket ${socketId} not found`); resolve(-1); return; } managed.socket.send(data, port, address, (err, bytes) => { if (err) { debug(`[${managed.pluginId}] Send error: ${err}`); resolve(-1); } else { resolve(bytes); } }); }); } /** * Receive data from buffer (non-blocking) * @param socketId - Socket to receive from * @returns Buffered datagram, or null if buffer empty */ receive(socketId) { const managed = this.sockets.get(socketId); if (!managed) { debug(`Socket ${socketId} not found`); return null; } return managed.receiveBuffer.shift() || null; } /** * Get number of buffered datagrams */ getBufferedCount(socketId) { const managed = this.sockets.get(socketId); return managed ? managed.receiveBuffer.length : 0; } /** * Close a socket * @param socketId - Socket to close */ close(socketId) { const managed = this.sockets.get(socketId); if (!managed) { debug(`Socket ${socketId} not found`); return; } try { // Leave all multicast groups first for (const group of managed.multicastGroups) { try { managed.socket.dropMembership(group); } catch (e) { // Ignore errors when leaving groups during close } } managed.socket.close(); this.sockets.delete(socketId); debug(`[${managed.pluginId}] Socket ${socketId} closed`); } catch (error) { debug(`[${managed.pluginId}] Close error: ${error}`); } } /** * Close all sockets for a plugin (cleanup on plugin stop) */ closeAllForPlugin(pluginId) { const toClose = []; for (const [id, managed] of this.sockets) { if (managed.pluginId === pluginId) { toClose.push(id); } } for (const id of toClose) { this.close(id); } debug(`[${pluginId}] Closed ${toClose.length} sockets`); } /** * Get socket statistics */ getStats() { const socketsPerPlugin = {}; for (const managed of this.sockets.values()) { socketsPerPlugin[managed.pluginId] = (socketsPerPlugin[managed.pluginId] || 0) + 1; } return { totalSockets: this.sockets.size, socketsPerPlugin }; } } // Export singleton instance exports.socketManager = new SocketManager(); /** * TCP Socket Manager - manages TCP connections for WASM plugins * * Key differences from UDP: * - Connection-oriented (connect before send) * - Line-buffered receive (splits on \r\n or \n) */ class TcpSocketManager { sockets = new Map(); nextSocketId = 1; /** * Create a new TCP socket * @param pluginId - Plugin that owns the socket * @returns Socket ID, or -1 on error */ createSocket(pluginId) { try { const socketId = this.nextSocketId++; const socket = new net.Socket(); const managed = { socket, pluginId, connected: false, connecting: false, receiveBuffer: [], rawBuffer: [], partialLine: '', maxBufferSize: 1000, error: null, useLineBuffering: true // Default to line buffering }; // Set up data handler socket.on('data', (data) => { if (managed.useLineBuffering) { // Line-buffered mode for text protocols managed.partialLine += data.toString(); // Split on line endings (\r\n or \n) const lines = managed.partialLine.split(/\r?\n/); // Last element is either empty (if data ended with newline) or partial managed.partialLine = lines.pop() || ''; // Add complete lines to buffer for (const line of lines) { if (line.length > 0) { if (managed.receiveBuffer.length >= managed.maxBufferSize) { managed.receiveBuffer.shift(); // Drop oldest } managed.receiveBuffer.push(line); } } } else { // Raw mode for binary protocols if (managed.rawBuffer.length >= managed.maxBufferSize) { managed.rawBuffer.shift(); // Drop oldest } managed.rawBuffer.push(Buffer.from(data)); } }); socket.on('connect', () => { managed.connected = true; managed.connecting = false; managed.error = null; debug(`[${pluginId}] TCP socket ${socketId} connected`); }); socket.on('error', (err) => { managed.error = err.message; managed.connected = false; managed.connecting = false; debug(`[${pluginId}] TCP socket ${socketId} error: ${err.message}`); }); socket.on('close', () => { managed.connected = false; managed.connecting = false; this.sockets.delete(socketId); debug(`[${pluginId}] TCP socket ${socketId} closed`); }); socket.on('end', () => { managed.connected = false; debug(`[${pluginId}] TCP socket ${socketId} ended by remote`); }); this.sockets.set(socketId, managed); debug(`[${pluginId}] Created TCP socket ${socketId}`); return socketId; } catch (error) { debug(`Failed to create TCP socket: ${error}`); return -1; } } /** * Connect to a remote host * @param socketId - Socket to connect * @param address - Remote host address * @param port - Remote port * @returns 0 if connection initiated, -1 on error */ connect(socketId, address, port) { const managed = this.sockets.get(socketId); if (!managed) { debug(`TCP socket ${socketId} not found`); return -1; } if (managed.connected || managed.connecting) { debug(`[${managed.pluginId}] TCP socket ${socketId} already connected/connecting`); return -1; } try { managed.connecting = true; managed.error = null; managed.socket.connect(port, address); debug(`[${managed.pluginId}] TCP socket ${socketId} connecting to ${address}:${port}`); return 0; } catch (error) { managed.connecting = false; managed.error = String(error); debug(`[${managed.pluginId}] TCP connect error: ${error}`); return -1; } } /** * Check if socket is connected * @param socketId - Socket to check * @returns 1 if connected, 0 if not, -1 if socket not found */ isConnected(socketId) { const managed = this.sockets.get(socketId); if (!managed) { return -1; } return managed.connected ? 1 : 0; } /** * Send data over TCP * @param socketId - Socket to use * @param data - Data to send * @returns Bytes sent, or -1 on error */ send(socketId, data) { return new Promise((resolve) => { const managed = this.sockets.get(socketId); if (!managed) { debug(`TCP socket ${socketId} not found`); resolve(-1); return; } if (!managed.connected) { debug(`[${managed.pluginId}] TCP socket ${socketId} not connected`); resolve(-1); return; } managed.socket.write(data, (err) => { if (err) { debug(`[${managed.pluginId}] TCP send error: ${err}`); resolve(-1); } else { resolve(data.length); } }); }); } /** * Receive a complete line (non-blocking) * @param socketId - Socket to receive from * @returns Complete line without line ending, or null if no complete line available */ receiveLine(socketId) { const managed = this.sockets.get(socketId); if (!managed) { debug(`TCP socket ${socketId} not found`); return null; } return managed.receiveBuffer.shift() || null; } /** * Receive raw data (non-blocking) * @param socketId - Socket to receive from * @returns Raw data buffer, or null if no data available */ receiveRaw(socketId) { const managed = this.sockets.get(socketId); if (!managed) { debug(`TCP socket ${socketId} not found`); return null; } return managed.rawBuffer.shift() || null; } /** * Set buffering mode * @param socketId - Socket to configure * @param lineBuffering - true for line-buffered (text), false for raw (binary) * @returns 0 on success, -1 on error */ setLineBuffering(socketId, lineBuffering) { const managed = this.sockets.get(socketId); if (!managed) { return -1; } managed.useLineBuffering = lineBuffering; debug(`[${managed.pluginId}] TCP socket ${socketId} buffering mode: ${lineBuffering ? 'line' : 'raw'}`); return 0; } /** * Get number of buffered items (lines or raw chunks) */ getBufferedCount(socketId) { const managed = this.sockets.get(socketId); if (!managed) return 0; return managed.useLineBuffering ? managed.receiveBuffer.length : managed.rawBuffer.length; } /** * Get last error message */ getError(socketId) { const managed = this.sockets.get(socketId); return managed ? managed.error : null; } /** * Close a TCP socket * @param socketId - Socket to close */ close(socketId) { const managed = this.sockets.get(socketId); if (!managed) { debug(`TCP socket ${socketId} not found`); return; } try { managed.socket.destroy(); this.sockets.delete(socketId); debug(`[${managed.pluginId}] TCP socket ${socketId} closed`); } catch (error) { debug(`[${managed.pluginId}] TCP close error: ${error}`); } } /** * Close all TCP sockets for a plugin */ closeAllForPlugin(pluginId) { const toClose = []; for (const [id, managed] of this.sockets) { if (managed.pluginId === pluginId) { toClose.push(id); } } for (const id of toClose) { this.close(id); } debug(`[${pluginId}] Closed ${toClose.length} TCP sockets`); } /** * Get TCP socket statistics */ getStats() { const socketsPerPlugin = {}; for (const managed of this.sockets.values()) { socketsPerPlugin[managed.pluginId] = (socketsPerPlugin[managed.pluginId] || 0) + 1; } return { totalSockets: this.sockets.size, socketsPerPlugin }; } } // Export TCP socket manager singleton exports.tcpSocketManager = new TcpSocketManager(); //# sourceMappingURL=socket-manager.js.map