UNPKG

@nodedaemon/core

Version:

Production-ready Node.js process manager with zero external dependencies

1,340 lines (1,330 loc) 134 kB
#!/usr/bin/env node #!/usr/bin/env node "use strict"; /** * Standalone daemon launcher for NodeDaemon * This file is used when starting the daemon in detached mode */ Object.defineProperty(exports, "__esModule", { value: true }); const NodeDaemonCore_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeDaemonCore = void 0; const net_1 = require("net"); const fs_1 = require("fs"); const util_1 = require("util"); const events_1 = require("events"); const ProcessOrchestrator_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProcessOrchestrator = void 0; const child_process_1 = require("child_process"); const cluster_1 = __importDefault(require("cluster")); const os_1 = require("os"); const events_1 = require("events"); const helpers_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateId = generateId; exports.ensureDir = ensureDir; exports.ensureFileDir = ensureFileDir; exports.isFile = isFile; exports.isDirectory = isDirectory; exports.parseMemoryString = parseMemoryString; exports.formatMemory = formatMemory; exports.formatUptime = formatUptime; exports.calculateExponentialBackoff = calculateExponentialBackoff; exports.debounce = debounce; exports.throttle = throttle; exports.sanitizeProcessName = sanitizeProcessName; exports.validateProcessConfig = validateProcessConfig; const crypto_1 = require("crypto"); const fs_1 = require("fs"); const path_1 = require("path"); function generateId() { return (0, crypto_1.randomUUID)(); } function ensureDir(dirPath) { if (!(0, fs_1.existsSync)(dirPath)) { (0, fs_1.mkdirSync)(dirPath, { recursive: true }); } } function ensureFileDir(filePath) { ensureDir((0, path_1.dirname)(filePath)); } function isFile(path) { try { return (0, fs_1.existsSync)(path) && (0, fs_1.statSync)(path).isFile(); } catch { return false; } } function isDirectory(path) { try { return (0, fs_1.existsSync)(path) && (0, fs_1.statSync)(path).isDirectory(); } catch { return false; } } function parseMemoryString(memory) { const units = { 'B': 1, 'KB': 1024, 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024 }; const match = memory.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)$/i); if (!match || !match[1] || !match[2]) { throw new Error(`Invalid memory format: ${memory}`); } const [, value, unit] = match; return Math.floor(parseFloat(value) * units[unit.toUpperCase()]); } function formatMemory(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } function formatUptime(ms) { const seconds = Math.floor(ms / 1000) % 60; const minutes = Math.floor(ms / (1000 * 60)) % 60; const hours = Math.floor(ms / (1000 * 60 * 60)) % 24; const days = Math.floor(ms / (1000 * 60 * 60 * 24)); if (days > 0) { return `${days}d ${hours}h ${minutes}m ${seconds}s`; } if (hours > 0) { return `${hours}h ${minutes}m ${seconds}s`; } if (minutes > 0) { return `${minutes}m ${seconds}s`; } return `${seconds}s`; } function calculateExponentialBackoff(restartCount, baseDelay, maxDelay) { const delay = baseDelay * Math.pow(2, restartCount); return Math.min(delay, maxDelay); } function debounce(func, wait) { let timeout = null; return (...args) => { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { timeout = null; func.apply(null, args); }, wait); }; } function throttle(func, limit) { let inThrottle = false; return (...args) => { if (!inThrottle) { func.apply(null, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } function sanitizeProcessName(name) { return name.replace(/[^a-zA-Z0-9-_]/g, '_'); } function validateProcessConfig(config) { if (!config || typeof config !== 'object') { throw new Error('Process config must be an object'); } if (!config.script || typeof config.script !== 'string') { throw new Error('Process config must have a script property'); } if (!isFile(config.script)) { throw new Error(`Script file does not exist: ${config.script}`); } if (config.instances !== undefined) { if (config.instances !== 'max' && (!Number.isInteger(config.instances) || config.instances < 1)) { throw new Error('instances must be a positive integer or "max"'); } } if (config.maxRestarts !== undefined) { if (!Number.isInteger(config.maxRestarts) || config.maxRestarts < 0) { throw new Error('maxRestarts must be a non-negative integer'); } } } return module.exports; })(); const env_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadEnvFile = loadEnvFile; exports.findEnvFile = findEnvFile; exports.mergeEnvConfigs = mergeEnvConfigs; const fs_1 = require("fs"); const path_1 = require("path"); function loadEnvFile(filePath) { const envConfig = {}; if (!(0, fs_1.existsSync)(filePath)) { return envConfig; } try { const content = (0, fs_1.readFileSync)(filePath, 'utf8'); const lines = content.split('\n'); for (const line of lines) { // Skip empty lines and comments const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith('#')) { continue; } // Parse KEY=VALUE format const separatorIndex = trimmedLine.indexOf('='); if (separatorIndex > 0) { const key = trimmedLine.substring(0, separatorIndex).trim(); const value = trimmedLine.substring(separatorIndex + 1).trim(); // Remove surrounding quotes if present const unquotedValue = value.replace(/^["']|["']$/g, ''); envConfig[key] = unquotedValue; } } } catch (error) { // Silently fail if env file cannot be read } return envConfig; } function findEnvFile(scriptPath, envFile) { const scriptDir = (0, path_1.dirname)(scriptPath); // If specific env file is provided if (envFile) { const envPath = (0, path_1.join)(scriptDir, envFile); return (0, fs_1.existsSync)(envPath) ? envPath : null; } // Look for common env file names in order of priority const envFiles = [ '.env.local', '.env.development', '.env.production', '.env' ]; // Check current directory first for (const file of envFiles) { const envPath = (0, path_1.join)(process.cwd(), file); if ((0, fs_1.existsSync)(envPath)) { return envPath; } } // Then check script directory for (const file of envFiles) { const envPath = (0, path_1.join)(scriptDir, file); if ((0, fs_1.existsSync)(envPath)) { return envPath; } } return null; } function mergeEnvConfigs(...configs) { const merged = {}; for (const config of configs) { Object.assign(merged, config); } return merged; } return module.exports; })(); const constants_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WEB_UI_STATIC_DIR = exports.WEB_UI_DIR = exports.DEFAULT_WEB_UI_CONFIG = exports.SIGNALS = exports.FILE_WATCH_IGNORE = exports.FILE_WATCH_DEBOUNCE = exports.FORCE_KILL_TIMEOUT = exports.GRACEFUL_SHUTDOWN_TIMEOUT = exports.CPU_THRESHOLD = exports.MEMORY_THRESHOLD = exports.HEALTH_CHECK_INTERVAL = exports.LOG_BUFFER_SIZE = exports.MAX_LOG_FILES = exports.MAX_LOG_SIZE = exports.LOG_LEVELS = exports.PROCESS_EVENTS = exports.RESTART_STRATEGIES = exports.DEFAULT_CONFIG = exports.IPC_SOCKET_PATH = exports.DAEMON_LOG = exports.LOG_DIR = exports.STATE_FILE = exports.NODEDAEMON_DIR = void 0; const path_1 = require("path"); const os_1 = require("os"); exports.NODEDAEMON_DIR = (0, path_1.join)((0, os_1.homedir)(), '.nodedaemon'); exports.STATE_FILE = (0, path_1.join)(exports.NODEDAEMON_DIR, 'state.json'); exports.LOG_DIR = (0, path_1.join)(exports.NODEDAEMON_DIR, 'logs'); exports.DAEMON_LOG = (0, path_1.join)(exports.LOG_DIR, 'daemon.log'); exports.IPC_SOCKET_PATH = process.platform === 'win32' ? '\\\\.\\pipe\\nodedaemon' : (0, path_1.join)(exports.NODEDAEMON_DIR, 'daemon.sock'); exports.DEFAULT_CONFIG = { instances: 1, maxRestarts: 10, restartDelay: 1000, maxRestartDelay: 30000, minUptime: 10000, // 10 seconds - minimum uptime to reset restart counter autoRestartOnCrash: true, autoRestartOnHighMemory: false, autoRestartOnHighCpu: false, memoryThreshold: '512MB', cpuThreshold: 80 }; exports.RESTART_STRATEGIES = { EXPONENTIAL_BACKOFF: 'exponential', FIXED_DELAY: 'fixed', LINEAR_BACKOFF: 'linear' }; exports.PROCESS_EVENTS = { START: 'start', STOP: 'stop', RESTART: 'restart', CRASH: 'crash', EXIT: 'exit' }; exports.LOG_LEVELS = { DEBUG: 'debug', INFO: 'info', WARN: 'warn', ERROR: 'error' }; exports.MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB exports.MAX_LOG_FILES = 5; exports.LOG_BUFFER_SIZE = 1000; exports.HEALTH_CHECK_INTERVAL = 30000; // 30 seconds exports.MEMORY_THRESHOLD = 512 * 1024 * 1024; // 512MB exports.CPU_THRESHOLD = 80; // 80% exports.GRACEFUL_SHUTDOWN_TIMEOUT = 30000; // 30 seconds exports.FORCE_KILL_TIMEOUT = 5000; // 5 seconds exports.FILE_WATCH_DEBOUNCE = 100; // 100ms exports.FILE_WATCH_IGNORE = [ 'node_modules/**', '.git/**', '*.log', '*.tmp', '.DS_Store', 'Thumbs.db' ]; exports.SIGNALS = { SIGTERM: 'SIGTERM', SIGINT: 'SIGINT', SIGKILL: 'SIGKILL', SIGHUP: 'SIGHUP' }; exports.DEFAULT_WEB_UI_CONFIG = { enabled: false, port: 8080, host: '127.0.0.1', auth: null }; exports.WEB_UI_DIR = (0, path_1.join)(exports.NODEDAEMON_DIR, 'web'); exports.WEB_UI_STATIC_DIR = (0, path_1.join)(exports.WEB_UI_DIR, 'static'); return module.exports; })(); class ProcessOrchestrator extends events_1.EventEmitter { processes = new Map(); childProcesses = new Map(); restartTimers = new Map(); logger; isShuttingDown = false; constructor(logger) { super(); this.logger = logger; this.setupClusterEventHandlers(); this.setupProcessEventHandlers(); } setupClusterEventHandlers() { if (cluster_1.default.isPrimary) { cluster_1.default.on('exit', (worker, code, signal) => { this.handleWorkerExit(worker, code, signal); }); cluster_1.default.on('disconnect', (worker) => { this.logger.warn(`Cluster worker ${worker.id} disconnected`, { workerId: worker.id }); }); } } setupProcessEventHandlers() { process.on('SIGTERM', () => this.gracefulShutdown()); process.on('SIGINT', () => this.gracefulShutdown()); process.on('SIGHUP', () => this.reloadAllProcesses()); } async startProcess(config) { if (this.isShuttingDown) { throw new Error('Cannot start process during shutdown'); } (0, helpers_1.validateProcessConfig)(config); const processId = (0, helpers_1.generateId)(); const processName = config.name || (0, helpers_1.sanitizeProcessName)(config.script); const instanceCount = this.resolveInstanceCount(config.instances); const strategy = this.determineStrategy(config); // Load environment from .env files let envConfig = {}; const envFilePath = (0, env_1.findEnvFile)(config.script, config.envFile); if (envFilePath) { envConfig = (0, env_1.loadEnvFile)(envFilePath); this.logger.info(`Loaded environment from ${envFilePath}`, { processName }); } // Merge with provided env vars (provided vars take precedence) const finalEnv = (0, env_1.mergeEnvConfigs)(envConfig, config.env || {}); const processInfo = { id: processId, name: processName, script: config.script, status: 'starting', restarts: 0, instances: [], config: { ...constants_1.DEFAULT_CONFIG, ...config, env: finalEnv }, createdAt: Date.now(), updatedAt: Date.now() }; this.processes.set(processId, processInfo); this.logger.info(`Starting process: ${processName}`, { processId, strategy, instances: instanceCount }); try { if (strategy === 'cluster' && instanceCount > 1) { await this.startClusterProcess(processInfo, instanceCount); } else { await this.startSingleProcess(processInfo, strategy); } processInfo.status = 'running'; processInfo.updatedAt = Date.now(); this.emit('processStarted', processInfo); return processId; } catch (error) { processInfo.status = 'errored'; processInfo.updatedAt = Date.now(); this.logger.error(`Failed to start process ${processName}`, { error: error.message, processId }); throw error; } } resolveInstanceCount(instances) { if (instances === 'max') { return (0, os_1.cpus)().length; } return (typeof instances === 'number') ? instances : constants_1.DEFAULT_CONFIG.instances; } determineStrategy(config) { if (config.instances && (config.instances === 'max' || config.instances > 1)) { return 'cluster'; } if (config.script.endsWith('.js') || config.script.endsWith('.mjs')) { return 'fork'; } return 'spawn'; } async startClusterProcess(processInfo, instanceCount) { if (!cluster_1.default.isPrimary) { throw new Error('Cluster processes can only be started from primary process'); } const promises = []; for (let i = 0; i < instanceCount; i++) { promises.push(this.startClusterInstance(processInfo, i)); } await Promise.all(promises); } startClusterInstance(processInfo, instanceIndex) { return new Promise((resolve, reject) => { const instanceId = (0, helpers_1.generateId)(); const instance = { id: instanceId, status: 'starting', restarts: 0 }; processInfo.instances.push(instance); cluster_1.default.setupPrimary({ exec: processInfo.script, args: processInfo.config.args || [], cwd: processInfo.config.cwd }); const worker = cluster_1.default.fork({ ...process.env, ...processInfo.config.env }); this.childProcesses.set(instanceId, worker); worker.on('online', () => { instance.pid = worker.process.pid; instance.status = 'running'; instance.uptime = Date.now(); this.logger.info(`Cluster instance ${instanceIndex} started`, { processId: processInfo.id, instanceId, pid: worker.process.pid }); resolve(); }); worker.on('exit', (code, signal) => { this.handleInstanceExit(processInfo, instance, code, signal); }); worker.on('error', (error) => { this.logger.error(`Cluster instance error`, { processId: processInfo.id, instanceId, error: error.message }); reject(error); }); setTimeout(() => { if (instance.status === 'starting') { reject(new Error(`Cluster instance ${instanceIndex} failed to start within timeout`)); } }, 30000); }); } async startSingleProcess(processInfo, strategy) { const instanceId = (0, helpers_1.generateId)(); const instance = { id: instanceId, status: 'starting', restarts: 0 }; processInfo.instances.push(instance); let childProcess; if (strategy === 'fork') { childProcess = (0, child_process_1.fork)(processInfo.script, processInfo.config.args || [], { cwd: processInfo.config.cwd, env: { ...process.env, ...processInfo.config.env }, silent: false }); } else { const interpreter = processInfo.config.interpreter || 'node'; childProcess = (0, child_process_1.spawn)(interpreter, [processInfo.script, ...(processInfo.config.args || [])], { cwd: processInfo.config.cwd, env: { ...process.env, ...processInfo.config.env }, stdio: ['inherit', 'inherit', 'inherit'] }); } this.childProcesses.set(instanceId, childProcess); return new Promise((resolve, reject) => { childProcess.on('spawn', () => { instance.pid = childProcess.pid; instance.status = 'running'; instance.uptime = Date.now(); this.logger.info(`Process started`, { processId: processInfo.id, instanceId, pid: childProcess.pid, strategy }); resolve(); }); childProcess.on('exit', (code, signal) => { this.handleInstanceExit(processInfo, instance, code, signal); }); childProcess.on('error', (error) => { this.logger.error(`Process error`, { processId: processInfo.id, instanceId, error: error.message }); reject(error); }); setTimeout(() => { if (instance.status === 'starting') { reject(new Error('Process failed to start within timeout')); } }, 30000); }); } handleWorkerExit(worker, code, signal) { const instanceId = this.findInstanceByPid(worker.process.pid); if (instanceId) { const processInfo = this.findProcessByInstanceId(instanceId); const instance = processInfo?.instances.find(i => i.id === instanceId); if (processInfo && instance) { this.handleInstanceExit(processInfo, instance, code, signal); } } } handleInstanceExit(processInfo, instance, code, signal) { instance.status = code === 0 ? 'stopped' : 'crashed'; this.childProcesses.delete(instance.id); // Calculate uptime const uptime = instance.uptime ? Date.now() - instance.uptime : 0; // Reset restart counter if process ran successfully for minimum uptime if (uptime >= (processInfo.config.minUptime || constants_1.DEFAULT_CONFIG.minUptime)) { instance.restarts = 0; this.logger.info(`Process ran successfully for ${(0, helpers_1.formatUptime)(uptime)}, resetting restart counter`, { processId: processInfo.id, instanceId: instance.id }); } this.logger.info(`Process instance exited`, { processId: processInfo.id, instanceId: instance.id, pid: instance.pid, code, signal, uptime: (0, helpers_1.formatUptime)(uptime), restarts: instance.restarts }); if (processInfo.status !== 'stopping' && !this.isShuttingDown) { if (instance.restarts < processInfo.config.maxRestarts) { this.scheduleRestart(processInfo, instance); } else { this.logger.error(`Process instance reached max restarts, stopping permanently`, { processId: processInfo.id, instanceId: instance.id, maxRestarts: processInfo.config.maxRestarts, totalRestarts: instance.restarts }); instance.status = 'errored'; processInfo.status = 'errored'; // Emit event for max restarts reached this.emit('maxRestartsReached', processInfo, instance); } } this.updateProcessStatus(processInfo); this.emit('instanceExit', processInfo, instance, code, signal); } scheduleRestart(processInfo, instance) { const delay = (0, helpers_1.calculateExponentialBackoff)(instance.restarts, processInfo.config.restartDelay, processInfo.config.maxRestartDelay); this.logger.warn(`Process crashed too quickly, scheduling restart in ${delay}ms`, { processId: processInfo.id, instanceId: instance.id, attempt: instance.restarts + 1, maxRestarts: processInfo.config.maxRestarts, backoffDelay: `${delay}ms` }); const timer = setTimeout(() => { this.restartInstance(processInfo, instance); }, delay); this.restartTimers.set(instance.id, timer); } async restartInstance(processInfo, instance) { try { this.restartTimers.delete(instance.id); instance.restarts++; instance.lastRestart = Date.now(); instance.status = 'starting'; this.logger.info(`Restarting process instance`, { processId: processInfo.id, instanceId: instance.id, attempt: instance.restarts }); const strategy = this.determineStrategy(processInfo.config); if (strategy === 'cluster') { await this.startClusterInstance(processInfo, 0); } else { await this.startSingleProcess(processInfo, strategy); } this.emit('instanceRestarted', processInfo, instance); } catch (error) { this.logger.error(`Failed to restart process instance`, { processId: processInfo.id, instanceId: instance.id, error: error.message }); instance.status = 'errored'; this.updateProcessStatus(processInfo); } } updateProcessStatus(processInfo) { const runningInstances = processInfo.instances.filter(i => i.status === 'running'); const stoppedInstances = processInfo.instances.filter(i => i.status === 'stopped'); const erroredInstances = processInfo.instances.filter(i => i.status === 'errored' || i.status === 'crashed'); if (runningInstances.length > 0) { processInfo.status = 'running'; } else if (erroredInstances.length > 0) { processInfo.status = 'errored'; } else if (stoppedInstances.length === processInfo.instances.length) { processInfo.status = 'stopped'; } else { processInfo.status = 'starting'; } processInfo.updatedAt = Date.now(); processInfo.restarts = processInfo.instances.reduce((sum, instance) => sum + instance.restarts, 0); } async stopProcess(processId, force = false) { const processInfo = this.processes.get(processId); if (!processInfo) { throw new Error(`Process not found: ${processId}`); } processInfo.status = 'stopping'; processInfo.updatedAt = Date.now(); this.logger.info(`Stopping process: ${processInfo.name}`, { processId, force }); const stopPromises = processInfo.instances.map(instance => this.stopInstance(processInfo, instance, force)); await Promise.all(stopPromises); processInfo.status = 'stopped'; processInfo.updatedAt = Date.now(); this.emit('processStopped', processInfo); } async stopInstance(processInfo, instance, force) { const childProcess = this.childProcesses.get(instance.id); if (!childProcess) return; return new Promise((resolve) => { const cleanup = () => { this.childProcesses.delete(instance.id); instance.status = 'stopped'; resolve(); }; if (force) { childProcess.kill('SIGKILL'); setTimeout(cleanup, 1000); return; } let killed = false; const killTimer = setTimeout(() => { if (!killed) { killed = true; childProcess.kill('SIGKILL'); setTimeout(cleanup, constants_1.FORCE_KILL_TIMEOUT); } }, constants_1.GRACEFUL_SHUTDOWN_TIMEOUT); childProcess.once('exit', () => { if (!killed) { killed = true; clearTimeout(killTimer); cleanup(); } }); childProcess.kill('SIGTERM'); }); } async restartProcess(processId, graceful = false) { const processInfo = this.processes.get(processId); if (!processInfo) { throw new Error(`Process not found: ${processId}`); } const strategy = this.determineStrategy(processInfo.config); const instanceCount = this.resolveInstanceCount(processInfo.config.instances); // For cluster mode with multiple instances, do graceful reload if (graceful && strategy === 'cluster' && instanceCount > 1) { await this.gracefulReload(processInfo); } else { // Standard restart await this.stopProcess(processId); const updatedProcessInfo = this.processes.get(processId); if (!updatedProcessInfo) { throw new Error(`Process not found after stop: ${processId}`); } updatedProcessInfo.instances = []; updatedProcessInfo.status = 'starting'; updatedProcessInfo.updatedAt = Date.now(); if (strategy === 'cluster' && instanceCount > 1) { await this.startClusterProcess(updatedProcessInfo, instanceCount); } else { await this.startSingleProcess(updatedProcessInfo, strategy); } updatedProcessInfo.status = 'running'; updatedProcessInfo.updatedAt = Date.now(); this.emit('processRestarted', updatedProcessInfo); } } async gracefulReload(processInfo) { this.logger.info(`Starting graceful reload for ${processInfo.name}`, { processId: processInfo.id }); const instanceCount = this.resolveInstanceCount(processInfo.config.instances); const oldInstances = [...processInfo.instances]; const newInstances = []; processInfo.status = 'reloading'; processInfo.updatedAt = Date.now(); // Start new instances first for (let i = 0; i < instanceCount; i++) { const instanceId = (0, helpers_1.generateId)(); const instance = { id: instanceId, status: 'starting', restarts: 0 }; newInstances.push(instance); processInfo.instances.push(instance); try { await this.startClusterInstance(processInfo, i); // Wait a bit for the new instance to be ready await new Promise(resolve => setTimeout(resolve, 2000)); } catch (error) { this.logger.error(`Failed to start new instance during graceful reload`, { processId: processInfo.id, instanceId, error: error.message }); } } // Now stop old instances one by one for (const oldInstance of oldInstances) { await this.stopInstance(processInfo, oldInstance, false); // Remove old instance from the list const index = processInfo.instances.findIndex(i => i.id === oldInstance.id); if (index >= 0) { processInfo.instances.splice(index, 1); } // Wait a bit between stopping instances await new Promise(resolve => setTimeout(resolve, 1000)); } processInfo.status = 'running'; processInfo.updatedAt = Date.now(); this.logger.info(`Graceful reload completed for ${processInfo.name}`, { processId: processInfo.id, oldInstances: oldInstances.length, newInstances: newInstances.length }); this.emit('processReloaded', processInfo); } deleteProcess(processId) { const processInfo = this.processes.get(processId); if (!processInfo) { throw new Error(`Process not found: ${processId}`); } if (processInfo.status === 'running' || processInfo.status === 'starting') { throw new Error('Cannot delete running process. Stop it first.'); } this.processes.delete(processId); this.logger.info(`Process deleted: ${processInfo.name}`, { processId }); this.emit('processDeleted', processInfo); } getProcesses() { return Array.from(this.processes.values()); } getProcess(processId) { return this.processes.get(processId); } getProcessByName(name) { return Array.from(this.processes.values()).find(p => p.name === name); } findInstanceByPid(pid) { for (const processInfo of this.processes.values()) { for (const instance of processInfo.instances) { if (instance.pid === pid) { return instance.id; } } } return undefined; } findProcessByInstanceId(instanceId) { for (const processInfo of this.processes.values()) { if (processInfo.instances.some(i => i.id === instanceId)) { return processInfo; } } return undefined; } async reloadAllProcesses() { const runningProcesses = Array.from(this.processes.values()) .filter(p => p.status === 'running'); this.logger.info(`Reloading ${runningProcesses.length} processes`); const reloadPromises = runningProcesses.map(async (processInfo) => { try { await this.restartProcess(processInfo.id); } catch (error) { this.logger.error(`Failed to reload process ${processInfo.name}`, { processId: processInfo.id, error: error.message }); } }); await Promise.all(reloadPromises); } async gracefulShutdown() { if (this.isShuttingDown) return; this.isShuttingDown = true; this.logger.info('Starting graceful shutdown'); const runningProcesses = Array.from(this.processes.values()) .filter(p => p.status === 'running' || p.status === 'starting'); if (runningProcesses.length === 0) { this.logger.info('No running processes to stop'); return; } this.logger.info(`Stopping ${runningProcesses.length} processes`); const stopPromises = runningProcesses.map(async (processInfo) => { try { await this.stopProcess(processInfo.id); } catch (error) { this.logger.error(`Failed to stop process ${processInfo.name}`, { processId: processInfo.id, error: error.message }); } }); await Promise.all(stopPromises); this.restartTimers.forEach(timer => clearTimeout(timer)); this.restartTimers.clear(); this.logger.info('Graceful shutdown completed'); } getHealthCheck() { const results = []; for (const processInfo of this.processes.values()) { for (const instance of processInfo.instances) { if (instance.status === 'running' && instance.pid) { try { const memUsage = process.memoryUsage(); const uptime = instance.uptime ? Date.now() - instance.uptime : 0; results.push({ processId: processInfo.id, memory: memUsage.rss, cpu: 0, // TODO: Implement CPU monitoring uptime, healthy: instance.status === 'running' }); } catch (error) { results.push({ processId: processInfo.id, memory: 0, cpu: 0, uptime: 0, healthy: false, issues: [`Health check failed: ${error.message}`] }); } } } } return results; } } exports.ProcessOrchestrator = ProcessOrchestrator; return module.exports; })(); const FileWatcher_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileWatcher = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const crypto_1 = require("crypto"); const events_1 = require("events"); const helpers_1 = (function() { const module = { exports: {} }; const exports = module.exports; return module.exports; })(); const constants_1 = (function() { const module = { exports: {} }; const exports = module.exports; return module.exports; })(); class FileWatcher extends events_1.EventEmitter { watchers = new Map(); watchedFiles = new Map(); debouncedHandlers = new Map(); ignorePatterns = []; isWatching = false; constructor() { super(); this.setupIgnorePatterns(); } setupIgnorePatterns() { this.ignorePatterns = constants_1.FILE_WATCH_IGNORE.map(pattern => { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); return new RegExp(regexPattern); }); } watch(paths, options = {}) { // Don't unwatch existing paths, just add new ones // This was the bug - it was unwatching all previous paths! const pathsArray = Array.isArray(paths) ? paths : [paths]; const { ignoreInitial = true, recursive = true, ignored = [] } = options; if (ignored.length > 0) { const additionalPatterns = ignored.map(pattern => { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); return new RegExp(regexPattern); }); this.ignorePatterns.push(...additionalPatterns); } pathsArray.forEach(watchPath => { this.watchPath((0, path_1.resolve)(watchPath), recursive); }); if (!ignoreInitial) { this.scanInitialFiles(pathsArray); } this.isWatching = true; } watchPath(path, recursive) { try { if (!(0, helpers_1.isDirectory)(path) && !(0, helpers_1.isFile)(path)) { return; } console.log(`[FileWatcher] Watching path: ${path} (recursive: ${recursive})`); const watcher = (0, fs_1.watch)(path, { recursive }, (eventType, filename) => { if (!filename) return; const fullPath = (0, path_1.resolve)(path, filename); console.log(`[FileWatcher] Event: ${eventType} on ${fullPath}`); this.handleFileEvent(eventType, fullPath); }); watcher.on('error', (error) => { this.emit('error', error); }); this.watchers.set(path, watcher); } catch (error) { this.emit('error', new Error(`Failed to watch path ${path}: ${error}`)); } } handleFileEvent(eventType, filePath) { if (this.shouldIgnore(filePath)) { return; } const debouncedHandler = this.getDebouncedHandler(filePath); debouncedHandler(eventType, filePath); } getDebouncedHandler(filePath) { if (!this.debouncedHandlers.has(filePath)) { const handler = (0, helpers_1.debounce)((eventType, path) => { this.processFileChange(eventType, path); }, constants_1.FILE_WATCH_DEBOUNCE); this.debouncedHandlers.set(filePath, handler); } return this.debouncedHandlers.get(filePath); } processFileChange(eventType, filePath) { try { const existingFile = this.watchedFiles.get(filePath); if (!(0, helpers_1.isFile)(filePath)) { if (existingFile) { this.watchedFiles.delete(filePath); this.emitFileEvent('unlink', filePath); } return; } const stats = (0, fs_1.statSync)(filePath); const currentHash = this.calculateFileHash(filePath); const currentSize = stats.size; const currentMtime = stats.mtime.getTime(); if (!existingFile) { this.watchedFiles.set(filePath, { path: filePath, hash: currentHash, size: currentSize, mtime: currentMtime }); this.emitFileEvent('add', filePath, stats); } else { const hasChanged = existingFile.hash !== currentHash || existingFile.size !== currentSize || existingFile.mtime !== currentMtime; if (hasChanged) { this.watchedFiles.set(filePath, { path: filePath, hash: currentHash, size: currentSize, mtime: currentMtime }); this.emitFileEvent('change', filePath, stats); } } } catch (error) { this.emit('error', new Error(`Failed to process file change for ${filePath}: ${error}`)); } } calculateFileHash(filePath) { try { const content = (0, fs_1.readFileSync)(filePath); return (0, crypto_1.createHash)('md5').update(content).digest('hex'); } catch (error) { return ''; } } shouldIgnore(filePath) { const relativePath = (0, path_1.relative)(process.cwd(), filePath); return this.ignorePatterns.some(pattern => pattern.test(relativePath)); } scanInitialFiles(paths) { paths.forEach(path => { this.scanDirectory((0, path_1.resolve)(path)); }); } scanDirectory(dirPath) { try { if (!(0, helpers_1.isDirectory)(dirPath)) { if ((0, helpers_1.isFile)(dirPath) && !this.shouldIgnore(dirPath)) { this.processFileChange('add', dirPath); } return; } const fs = require('fs'); const entries = fs.readdirSync(dirPath, { withFileTypes: true }); entries.forEach((entry) => { const entryPath = (0, path_1.join)(dirPath, entry.name); if (this.shouldIgnore(entryPath)) { return; } if (entry.isDirectory()) { this.scanDirectory(entryPath); } else if (entry.isFile()) { this.processFileChange('add', entryPath); } }); } catch (error) { this.emit('error', new Error(`Failed to scan directory ${dirPath}: ${error}`)); } } emitFileEvent(type, filePath, stats) { const event = { type, filename: require('path').basename(filePath), path: filePath, stats }; this.emit('fileChange', event); this.emit(type, filePath, stats); } unwatch() { this.watchers.forEach(watcher => { try { watcher.close(); } catch (error) { this.emit('error', error); } }); this.watchers.clear(); this.watchedFiles.clear(); this.debouncedHandlers.clear(); this.isWatching = false; } getWatchedFiles() { return Array.from(this.watchedFiles.keys()); } isFileWatched(filePath) { return this.watchedFiles.has((0, path_1.resolve)(filePath)); } getFileInfo(filePath) { return this.watchedFiles.get((0, path_1.resolve)(filePath)); } addIgnorePattern(pattern) { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); this.ignorePatterns.push(new RegExp(regexPattern)); } removeIgnorePattern(pattern) { const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); const regex = new RegExp(regexPattern); this.ignorePatterns = this.ignorePatterns.filter(existingPattern => existingPattern.source !== regex.source); } clearIgnorePatterns() { this.ignorePatterns = []; this.setupIgnorePatterns(); } getIgnorePatterns() { return this.ignorePatterns.map(pattern => pattern.source); } pause() { this.watchers.forEach(watcher => { try { watcher.close(); } catch (error) { this.emit('error', error); } }); this.isWatching = false; } resume() { if (!this.isWatching) { const watchedPaths = Array.from(this.watchers.keys()); this.watchers.clear(); watchedPaths.forEach(path => { this.watchPath(path, true); }); this.isWatching = true; } } getStats() { return { watchedPaths: this.watchers.size, watchedFiles: this.watchedFiles.size, isWatching: this.isWatching, ignorePatterns: this.ignorePatterns.length }; } } exports.FileWatcher = FileWatcher; return module.exports; })(); const LogManager_1 = (function() { const module = { exports: {} }; const exports = module.exports; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LogManager = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const zlib_1 = require("zlib"); const events_1 = require("events"); const helpers_1 = (function() { const module = { exports: {} }; const exports = module.exports; return module.exports; })(); const constants_1 = (function() { const module = { exports: {} }; const exports = module.exports; return module.exports; })(); class LogManager extends events_1.EventEmitter { logStreams = new Map(); logBuffer = []; bufferIndex = 0; isShuttingDown = false; constructor() { super(); (0, helpers_1.ensureDir)(constants_1.LOG_DIR); this.setupMainLogStream(); } setupMainLogStream() { const mainLogPath = (0, path_1.join)(constants_1.LOG_DIR, 'daemon.log'); this.createLogStream('daemon', mainLogPath); } createLogStream(name, filePath) { if (this.logStreams.has(name)) { this.logStreams.get(name)?.end(); } const stream = (0, fs_1.createWriteStream)(filePath, { flags: 'a' }); stream.on('error', (error) => { console.error(`Log stream error for ${name}:`, error); }); this.logStreams.set(name, stream); return stream; } log(entry) { if (this.isShuttingDown) return; this.addToBuffer(entry); // Emit log event for WebUI this.emit('log', entry); const logLine = this.formatLogEntry(entry); const streamName = entry.processId || 'daemon'; let stream = this.logStreams.get(streamName); if (!stream) { const logPath = (0, path_1.join)(constants_1.LOG_DIR, `${streamName}.log`); stream = this.createLogStream(streamName, logPath); } stream.write(logLine + '\n', (error) => { if (error) { console.error(`Failed to write log for ${streamName}:`, error); } }); this.checkLogRotation(streamName); } info(message, data, processId) { this.log({ timestamp: Date.now(), level: 'info', processId, message, data }); } warn(message, data, processId) { this.log({ timestamp: Date.now(), level: 'warn', processId, message, data }); } error(message, data, processId) { this.log({ timestamp: Date.now(), level: 'error', processId, message, data }); } debug(message, data, processId) { this.log({ timestamp: Date.now(), level: 'debug', processId, message, data }); } addToBuffer(entry) { if (this.logBuffer.length < constants_1.LOG_BUFFER_SIZE) { this.logBuffer.push(entry); } else { this.logBuffer[this.bufferIndex] = entry; this.bufferIndex = (this.bufferIndex + 1) % constants_1.LOG_BUFFER_SIZE; } } getRecentLogs(count = 100, processId) { let logs = [...this.logBuffer]; if (processId) { logs = logs.filter(log => log.processId === processId); } return logs .sort((a, b) => b.timestamp - a.timestamp) .slice(0, count); } formatLogEntry(entry) { const timestamp = new Date(entry.timestamp).toISOString(); const level = entry.level.toUpperCase().padEnd(5); const processInfo = entry.processId ? `[${entry.processId}] ` : ''; const dataInfo = entry.data ? ` ${JSON.stringify(entry.data)}` : ''; return `${timestamp} ${level} ${processInfo}${entry.message}${dataInfo}`; } checkLogRotation(streamName) { const logPath = (0, path_1.join)(constants_1.LOG_DIR, `${streamName}.log`); if (!(0, fs_1.existsSync)(logPath)) return; try { const stats = (0, fs_1.statSync)(logPath); if (stats.size > constants_1.MAX_LOG_SIZE) { this.rotateLog(streamName, logPath); } } catch (error) { console.error(`Failed to check log size for ${streamName}:`, error); } } rotateLog(streamName, currentLogPath) { try { const stream = this.logStreams.get(streamName); if (stream) { stream.end(); this.logStreams.delete(streamName); } this.rotateLogFiles(currentLogPath); this.createLogStream(streamName, currentLogPath); this.info(`Log rotated for ${streamName}`); } catch (error) { console.error(`Failed to rotate log for ${streamName}:`, error); } } rotateLogFiles(logPath) { const basePath = logPath.replace('.log', ''); for (let i = constants_1.MAX_LOG_FILES - 1; i > 0; i--) { const oldPath = i === 1 ? logPath : `${basePath}.${i}.log.gz`; const newPath = `${basePath}.${i + 1}.log.gz`; if ((0, fs_1.existsSync)(oldPath)) { if (i === constants_1.MAX_LOG_FILES - 1