UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

265 lines 10.6 kB
/** * WebSocket Server builtin for CLM execution. * * Provides deploy, status, and stop actions for WebSocket servers. * Uses Node.js 'ws' library. */ import { execSync, spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; /** * Resolve environment variables in config values like '${VAR}'. */ function resolveConfigValue(val) { if (typeof val === 'string' && val.startsWith('${') && val.endsWith('}')) { const envVar = val.slice(2, -1); return process.env[envVar] || val; } return val; } /** * Check if a port is in use using lsof (handles both IPv4 and IPv6). */ function isPortInUse(port) { try { const result = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); return result.trim().length > 0; } catch { return false; } } /** * Get PID of process using a port. */ function getPidOnPort(port) { try { const result = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); const pid = parseInt(result.trim().split('\n')[0], 10); return isNaN(pid) ? null : pid; } catch { return null; } } /** * Execute WebSocket server builtin. */ export async function executeWebSocketServer(config, context, chapterDir) { const action = context.action || config.action || 'status'; // Default to WS_PORT from .env matching the project configuration (5321) const defaultPort = parseInt(process.env.WS_PORT || '5321', 10); const rawPort = context.port || config.port || defaultPort; const port = parseInt(String(resolveConfigValue(rawPort)), 10); const host = context.host || config.host || 'localhost'; // Resolve project root let projectRoot = chapterDir; while (projectRoot !== path.dirname(projectRoot)) { if (fs.existsSync(path.join(projectRoot, 'package.json')) || fs.existsSync(path.join(projectRoot, '.git'))) { break; } projectRoot = path.dirname(projectRoot); } if (projectRoot === path.dirname(projectRoot)) { projectRoot = process.cwd(); } const pidFile = path.join(projectRoot, `.websocket_server_${port}.pid`); if (action === 'deploy') { // Check if already running if (isPortInUse(port)) { const pid = getPidOnPort(port); return { success: true, message: 'WebSocket server already running', pid, port, url: `ws://${host}:${port}/`, status: 'already_running' }; } try { // Locate the worker script // In development (src), it's in ./server-scripts/websocket-server-worker.ts // In production (dist), it's in ./server-scripts/websocket-server-worker.js // Current file is .../builtins/websocket-server.ts const currentDir = path.dirname(new URL(import.meta.url).pathname); // Construct path to worker script // We look for .js first (production), then fallback (though .ts won't run directly without loader) let workerScript = path.join(currentDir, 'server-scripts', 'websocket-server-worker.js'); // Verify if exists, otherwise try checking source location if running via tsx/ts-node (dev mode) if (!fs.existsSync(workerScript)) { // Try .ts for dev mode const workerScriptTs = path.join(currentDir, 'server-scripts', 'websocket-server-worker.ts'); if (fs.existsSync(workerScriptTs)) { workerScript = workerScriptTs; } else { return { success: false, error: `Worker script not found: ${workerScript}` }; } } // Create log file for WebSocket server output const logFile = path.join(projectRoot, `.websocket_server_${port}.log`); // Open log file with file descriptor for stdio const logFd = fs.openSync(logFile, 'a'); // Spawn Node.js WebSocket server as detached background process // Passing environment variables for configuration const env = { ...process.env, PORT: String(port), HOST: host }; // If running a .ts file (dev mode with tsx), we might need to use the loader // But easiest way for now is to rely on compilation. // If checking fails, we assume standard node execution on the resolved path. // For tsx support (dev), we'd need to spawn with tsx if the extension is .ts const isTs = workerScript.endsWith('.ts'); const execArgv = isTs && process.execArgv.includes('--loader') ? process.execArgv : []; // Better: use process.execPath and let it handle it, or use 'tsx' if available? // "spawn(process.execPath...)" runs 'node'. 'node file.ts' fails. // We should rely on the build step having run, OR use tsx if we are in dev. let child; if (isTs) { // Dev mode: assume tsx is available or we are in a ts-node env // We'll try to use 'npx tsx' or similar if we really had to, but the requirement is "native implementation". // Since we are compiling, we should target the .js file. // However, the `demo:clm` command uses `tsx`. // If we run `tsx examples/run-clm.ts`, we are in dev mode. // We should assume proper environment. // For robustness, let's try to detect if we should use tsx child = spawn('npx', ['tsx', workerScript], { detached: true, stdio: ['ignore', logFd, logFd], env }); } else { // Production mode: node running .js child = spawn(process.execPath, [workerScript], { detached: true, stdio: ['ignore', logFd, logFd], env }); } // Log any errors from the child process child.on('error', (err) => { console.error(`[WebSocket Server] Process error: ${err}`); fs.appendFileSync(logFile, `\nProcess error: ${err}\n`); }); child.on('exit', (code, signal) => { if (code !== null && code !== 0) { console.error(`[WebSocket Server] Process exited with code ${code}`); fs.appendFileSync(logFile, `\nProcess exited with code ${code}\n`); } // Close file descriptor on exit try { fs.closeSync(logFd); } catch { } }); child.unref(); // Close file descriptor in parent process try { fs.closeSync(logFd); } catch { } // Save PID fs.writeFileSync(pidFile, String(child.pid)); // Wait for server to start await new Promise(resolve => setTimeout(resolve, 3000)); if (isPortInUse(port)) { console.log(`[WebSocket Server] Successfully started on port ${port}`); return { success: true, message: 'WebSocket server deployed successfully', pid: child.pid, port, url: `ws://${host}:${port}/`, status: 'running', logFile }; } else { // Read log file to get error details let errorDetails = ''; try { errorDetails = fs.readFileSync(logFile, 'utf8'); } catch { } return { success: false, error: "Server started but not responding. Check log file for details.", logFile, details: errorDetails.slice(-500) }; } } catch (error) { return { success: false, error: `Failed to start server: ${error}` }; } } if (action === 'status') { const isRunning = isPortInUse(port); const pid = getPidOnPort(port); let savedPid = null; if (fs.existsSync(pidFile)) { try { savedPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); } catch { } } return { success: true, running: isRunning, pid, saved_pid: savedPid, port, url: isRunning ? `ws://${host}:${port}/` : null, status: isRunning ? 'running' : 'stopped' }; } if (action === 'stop') { const pid = getPidOnPort(port); if (!pid) { if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } return { success: true, message: 'No WebSocket server running on this port', port, status: 'stopped' }; } try { process.kill(pid, 'SIGTERM'); await new Promise(resolve => setTimeout(resolve, 500)); if (isPortInUse(port)) { process.kill(pid, 'SIGKILL'); await new Promise(resolve => setTimeout(resolve, 500)); } if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } return { success: true, message: 'WebSocket server stopped', pid, port, status: 'stopped' }; } catch (error) { if (error.code === 'ESRCH') { if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } return { success: true, message: 'WebSocket server was not running', port, status: 'stopped' }; } return { success: false, error: `Failed to stop server: ${error}` }; } } return { success: false, error: `Unknown action: ${action}` }; } //# sourceMappingURL=websocket-server.js.map