UNPKG

neex

Version:

Neex - Modern Fullstack Framework Built on Express and Next.js. Fast to Start, Easy to Build, Ready to Deploy

670 lines (669 loc) 28 kB
"use strict"; 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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.ProcessManager = void 0; // src/process-manager.ts - Enhanced PM2-like process manager const child_process_1 = require("child_process"); const fs = __importStar(require("fs")); const fs_1 = require("fs"); const path = __importStar(require("path")); const events_1 = require("events"); const chalk_1 = __importDefault(require("chalk")); const figures_1 = __importDefault(require("figures")); const logger_js_1 = __importDefault(require("./logger.js")); class ProcessManager extends events_1.EventEmitter { constructor(configPath) { super(); this.processes = new Map(); this.logStreams = new Map(); this.isMonitoring = false; this.isDisposed = false; const baseDir = path.join(process.cwd(), '.neex'); this.configPath = configPath || path.join(baseDir, 'processes.json'); this.logDir = path.join(baseDir, 'logs'); this.pidDir = path.join(baseDir, 'pids'); this.ensureDirectories(); this.startMonitoring(); } async ensureDirectories() { try { const dirs = [ path.dirname(this.configPath), this.logDir, this.pidDir ]; for (const dir of dirs) { await fs_1.promises.mkdir(dir, { recursive: true }); } } catch (error) { logger_js_1.default.printLine(`Failed to create directories: ${error.message}`, 'error'); } } async saveConfig() { try { const config = Array.from(this.processes.values()).map(proc => { var _a, _b; return ({ ...proc.config, status: proc.status, created_at: proc.created_at.toISOString(), started_at: (_a = proc.started_at) === null || _a === void 0 ? void 0 : _a.toISOString(), restarts: proc.restarts, last_restart: (_b = proc.last_restart) === null || _b === void 0 ? void 0 : _b.toISOString() }); }); await fs_1.promises.writeFile(this.configPath, JSON.stringify(config, null, 2)); } catch (error) { logger_js_1.default.printLine(`Failed to save config: ${error.message}`, 'error'); } } async load() { try { const data = await fs_1.promises.readFile(this.configPath, 'utf-8'); const configs = JSON.parse(data); for (const config of configs) { if (config.id && !this.processes.has(config.id)) { const processInfo = { id: config.id, name: config.name, status: 'stopped', uptime: 0, restarts: config.restarts || 0, memory: 0, cpu: 0, created_at: config.created_at ? new Date(config.created_at) : new Date(), started_at: config.started_at ? new Date(config.started_at) : undefined, last_restart: config.last_restart ? new Date(config.last_restart) : undefined, config: { ...config, // Ensure required fields have defaults autorestart: config.autorestart !== false, max_restarts: config.max_restarts || 10, restart_delay: config.restart_delay || 1000, instances: config.instances || 1 } }; this.processes.set(config.id, processInfo); // Check if process is actually running if (config.pid) { const isRunning = await this.isProcessRunning(config.pid); if (isRunning) { processInfo.status = 'online'; processInfo.pid = config.pid; processInfo.started_at = processInfo.started_at || new Date(); } } } } } catch (error) { // Config file might not exist yet, that's fine } } async isProcessRunning(pid) { try { process.kill(pid, 0); return true; } catch (error) { return false; } } generateId(name) { let id = name.toLowerCase().replace(/[^a-z0-9-_]/g, '-'); let counter = 0; while (this.processes.has(id)) { counter++; id = `${name.toLowerCase().replace(/[^a-z0-9-_]/g, '-')}-${counter}`; } return id; } setupLogStreams(processInfo) { const { config } = processInfo; // Close existing streams this.closeLogStreams(processInfo.id); // Setup default log paths if (!config.out_file) { config.out_file = path.join(this.logDir, `${processInfo.id}-out.log`); } if (!config.error_file) { config.error_file = path.join(this.logDir, `${processInfo.id}-error.log`); } try { const outStream = fs.createWriteStream(config.out_file, { flags: 'a' }); const errStream = fs.createWriteStream(config.error_file, { flags: 'a' }); this.logStreams.set(processInfo.id, { out: outStream, err: errStream }); } catch (error) { logger_js_1.default.printLine(`Failed to create log streams for ${processInfo.id}: ${error.message}`, 'error'); } } closeLogStreams(id) { var _a, _b; const streams = this.logStreams.get(id); if (streams) { (_a = streams.out) === null || _a === void 0 ? void 0 : _a.end(); (_b = streams.err) === null || _b === void 0 ? void 0 : _b.end(); this.logStreams.delete(id); } } async writePidFile(processInfo) { if (!processInfo.pid) return; const pidFile = path.join(this.pidDir, `${processInfo.id}.pid`); try { await fs_1.promises.writeFile(pidFile, processInfo.pid.toString()); } catch (error) { logger_js_1.default.printLine(`Failed to write PID file: ${error.message}`, 'error'); } } async removePidFile(id) { const pidFile = path.join(this.pidDir, `${id}.pid`); try { await fs_1.promises.unlink(pidFile); } catch (error) { // PID file might not exist, ignore } } parseCommand(script) { // Handle different script formats if (script.includes(' ')) { const parts = script.split(' '); return { command: parts[0], args: parts.slice(1) }; } // Handle file extensions if (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.cjs')) { return { command: 'node', args: [script] }; } if (script.endsWith('.ts') || script.endsWith('.mts') || script.endsWith('.cts')) { return { command: 'npx', args: ['ts-node', script] }; } return { command: script, args: [] }; } async startProcess(processInfo) { const { config } = processInfo; try { processInfo.status = 'launching'; processInfo.started_at = new Date(); // Setup logging this.setupLogStreams(processInfo); // Parse command const { command, args } = this.parseCommand(config.script); const finalArgs = [...args, ...(config.args || [])]; // Setup environment const env = { ...process.env, ...config.env, NEEX_PROCESS_ID: processInfo.id, NEEX_PROCESS_NAME: processInfo.name, NODE_ENV: process.env.NODE_ENV || 'production' }; // Spawn process const childProcess = (0, child_process_1.spawn)(command, finalArgs, { cwd: config.cwd || process.cwd(), env, stdio: ['ignore', 'pipe', 'pipe'], detached: true // Detach the process to run independently }); if (!childProcess.pid) { throw new Error(`Failed to start process: ${config.script}`); } processInfo.process = childProcess; processInfo.pid = childProcess.pid; processInfo.status = 'online'; // Allow the parent process to exit independently of the child childProcess.unref(); // Write PID file await this.writePidFile(processInfo); console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${processInfo.id}) started with PID ${processInfo.pid}`)); this.emit('process:start', processInfo); // Setup logging const streams = this.logStreams.get(processInfo.id); if (childProcess.stdout && (streams === null || streams === void 0 ? void 0 : streams.out)) { childProcess.stdout.pipe(streams.out); childProcess.stdout.on('data', (data) => { var _a; const message = data.toString(); if (config.time) { const timestamp = new Date().toISOString(); (_a = streams.out) === null || _a === void 0 ? void 0 : _a.write(`[${timestamp}] ${message}`); } this.emit('process:log', { id: processInfo.id, type: 'stdout', data: message }); }); } if (childProcess.stderr && (streams === null || streams === void 0 ? void 0 : streams.err)) { childProcess.stderr.pipe(streams.err); childProcess.stderr.on('data', (data) => { var _a; const message = data.toString(); if (config.time) { const timestamp = new Date().toISOString(); (_a = streams.err) === null || _a === void 0 ? void 0 : _a.write(`[${timestamp}] ${message}`); } this.emit('process:log', { id: processInfo.id, type: 'stderr', data: message }); }); } // Handle process exit childProcess.on('exit', async (code, signal) => { processInfo.exit_code = code || undefined; processInfo.exit_signal = signal || undefined; processInfo.status = code === 0 ? 'stopped' : 'errored'; processInfo.process = undefined; processInfo.pid = undefined; await this.removePidFile(processInfo.id); const exitMsg = signal ? `Process ${processInfo.name} killed by signal ${signal}` : `Process ${processInfo.name} exited with code ${code}`; console.log(chalk_1.default.yellow(`${figures_1.default.warning} ${exitMsg}`)); this.emit('process:exit', { processInfo, code, signal }); // Auto-restart if enabled and not manually stopped if (config.autorestart && processInfo.status === 'errored' && processInfo.restarts < (config.max_restarts || 10)) { const delay = config.restart_delay || 1000; console.log(chalk_1.default.blue(`${figures_1.default.info} Restarting ${processInfo.name} in ${delay}ms...`)); setTimeout(async () => { try { await this.restart(processInfo.id); } catch (error) { console.error(chalk_1.default.red(`${figures_1.default.cross} Failed to restart ${processInfo.name}: ${error.message}`)); } }, delay); } await this.saveConfig(); }); // Handle process error childProcess.on('error', (error) => { processInfo.status = 'errored'; console.error(chalk_1.default.red(`${figures_1.default.cross} Process ${processInfo.name} error: ${error.message}`)); this.emit('process:error', { processInfo, error }); }); await this.saveConfig(); } catch (error) { processInfo.status = 'errored'; console.error(chalk_1.default.red(`${figures_1.default.cross} Failed to start ${processInfo.name}: ${error.message}`)); this.emit('process:error', { processInfo, error }); throw error; } } startMonitoring() { if (this.isMonitoring || this.isDisposed) return; this.isMonitoring = true; this.monitoringInterval = setInterval(async () => { await this.updateProcessStats(); }, 5000); } stopMonitoring() { if (this.monitoringInterval) { clearInterval(this.monitoringInterval); this.monitoringInterval = undefined; } this.isMonitoring = false; } async updateProcessStats() { for (const processInfo of this.processes.values()) { if (processInfo.status === 'online' && processInfo.pid) { try { // Check if process is still running const isRunning = await this.isProcessRunning(processInfo.pid); if (!isRunning) { processInfo.status = 'stopped'; processInfo.process = undefined; processInfo.pid = undefined; await this.removePidFile(processInfo.id); continue; } // Update uptime if (processInfo.started_at) { processInfo.uptime = Math.floor((Date.now() - processInfo.started_at.getTime()) / 1000); } // Get memory and CPU usage (Unix/Linux only) if (process.platform !== 'win32') { try { const stats = await this.getProcessStats(processInfo.pid); processInfo.memory = stats.memory; processInfo.cpu = stats.cpu; } catch (error) { // Process might have died } } } catch (error) { // Process monitoring error } } } } async getProcessStats(pid) { return new Promise((resolve, reject) => { (0, child_process_1.exec)(`ps -o pid,rss,pcpu -p ${pid}`, (error, stdout) => { if (error) { resolve({ memory: 0, cpu: 0 }); return; } const lines = stdout.trim().split('\n'); if (lines.length < 2) { resolve({ memory: 0, cpu: 0 }); return; } const stats = lines[1].trim().split(/\s+/); const memory = parseInt(stats[1]) * 1024; // Convert KB to bytes const cpu = parseFloat(stats[2]); resolve({ memory, cpu }); }); }); } // Public API methods async start(config) { const id = this.generateId(config.name); const processInfo = { id, name: config.name, status: 'stopped', uptime: 0, restarts: 0, memory: 0, cpu: 0, created_at: new Date(), config: { ...config, id } }; this.processes.set(id, processInfo); await this.startProcess(processInfo); return id; } async stop(id) { const processInfo = this.processes.get(id); if (!processInfo) { throw new Error(`Process ${id} not found`); } if (processInfo.status === 'stopped') { console.log(chalk_1.default.yellow(`${figures_1.default.info} Process ${id} is already stopped`)); return; } processInfo.status = 'stopping'; console.log(chalk_1.default.blue(`${figures_1.default.info} Stopping process ${processInfo.name} (${id})...`)); if (processInfo.process && processInfo.pid) { try { // Try graceful shutdown first processInfo.process.kill('SIGTERM'); // Wait for graceful shutdown await new Promise(resolve => setTimeout(resolve, 2000)); // Force kill if still running if (processInfo.process && !processInfo.process.killed) { processInfo.process.kill('SIGKILL'); } } catch (error) { // Process might already be dead } } processInfo.status = 'stopped'; processInfo.process = undefined; processInfo.pid = undefined; await this.removePidFile(id); this.closeLogStreams(id); await this.saveConfig(); console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${id}) stopped`)); this.emit('process:stop', processInfo); } async restart(id) { const processInfo = this.processes.get(id); if (!processInfo) { throw new Error(`Process ${id} not found`); } console.log(chalk_1.default.blue(`${figures_1.default.info} Restarting process ${processInfo.name} (${id})...`)); if (processInfo.status === 'online') { await this.stop(id); // Wait a bit before restarting await new Promise(resolve => setTimeout(resolve, 1000)); } processInfo.restarts++; processInfo.last_restart = new Date(); await this.startProcess(processInfo); } async delete(id) { const processInfo = this.processes.get(id); if (!processInfo) { throw new Error(`Process ${id} not found`); } if (processInfo.status === 'online') { await this.stop(id); } // Remove log files try { if (processInfo.config.out_file) { await fs_1.promises.unlink(processInfo.config.out_file); } if (processInfo.config.error_file) { await fs_1.promises.unlink(processInfo.config.error_file); } } catch (error) { // Log files might not exist } this.closeLogStreams(id); this.processes.delete(id); await this.saveConfig(); console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${id}) deleted`)); this.emit('process:delete', processInfo); } async list() { return Array.from(this.processes.values()); } async logs(id, lines = 15) { const processInfo = this.processes.get(id); if (!processInfo) { throw new Error(`Process ${id} not found`); } return this.readLogFiles(processInfo.config, lines); } async readLogFiles(config, lines) { const logs = []; const readLastLines = async (filePath) => { try { const content = await fs_1.promises.readFile(filePath, 'utf-8'); const fileLines = content.split('\n').filter(line => line.trim() !== ''); return fileLines.slice(-lines); } catch (error) { return []; } }; if (config.out_file) { const outLogs = await readLastLines(config.out_file); logs.push(...outLogs.map(line => `[OUT] ${line}`)); } if (config.error_file) { const errLogs = await readLastLines(config.error_file); logs.push(...errLogs.map(line => `[ERR] ${line}`)); } return logs.slice(-lines); } followLogs(id, onLogEntry) { const processInfo = this.processes.get(id); if (!processInfo) { logger_js_1.default.printLine(`Process ${id} not found for log following.`, 'error'); return undefined; } const { config } = processInfo; const logFilesToWatch = []; // Resolve log file paths, defaulting if not specified (consistent with setupLogStreams) const outFile = config.out_file || path.join(this.logDir, `${processInfo.id}-out.log`); const errFile = config.error_file || path.join(this.logDir, `${processInfo.id}-error.log`); logFilesToWatch.push({ type: 'OUT', path: outFile }); if (errFile !== outFile) { // Avoid watching the same file path twice logFilesToWatch.push({ type: 'ERR', path: errFile }); } const lastReadSizes = {}; const setupWatcherForFile = (filePath, type) => { try { const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : { size: 0 }; lastReadSizes[filePath] = stats.size; } catch (err) { lastReadSizes[filePath] = 0; // logger.printLine(`Could not get initial stats for ${filePath}: ${(err as Error).message}. Assuming size 0.`, 'debug'); } const listener = (curr, prev) => { if (curr.mtimeMs <= prev.mtimeMs && curr.size === prev.size) { return; } const newSize = curr.size; let oldSize = lastReadSizes[filePath]; // Fallback for oldSize, though it should be initialized if (typeof oldSize !== 'number') oldSize = 0; if (newSize > oldSize) { const stream = fs.createReadStream(filePath, { start: oldSize, end: newSize - 1, encoding: 'utf-8', }); stream.on('data', (chunk) => { chunk.split('\n').forEach(line => { if (line.trim() !== '') { onLogEntry(`[${type}] ${line}`); } }); }); stream.on('error', (streamErr) => { logger_js_1.default.printLine(`Error reading log chunk for ${filePath}: ${streamErr.message}`, 'error'); }); lastReadSizes[filePath] = newSize; } else if (newSize < oldSize) { // File was truncated or replaced // logger.printLine(`Log file ${filePath} was truncated. Reading from start.`, 'debug'); lastReadSizes[filePath] = 0; const stream = fs.createReadStream(filePath, { start: 0, end: newSize - 1, encoding: 'utf-8', }); stream.on('data', (chunk) => { chunk.split('\n').forEach(line => { if (line.trim() !== '') { onLogEntry(`[${type}] ${line}`); } }); }); stream.on('error', (streamErr) => { logger_js_1.default.printLine(`Error reading truncated log file ${filePath}: ${streamErr.message}`, 'error'); }); lastReadSizes[filePath] = newSize; } }; try { fs.watchFile(filePath, { persistent: true, interval: 500 }, listener); } catch (watchError) { logger_js_1.default.printLine(`Failed to watch log file ${filePath}: ${watchError.message}`, 'error'); } }; logFilesToWatch.forEach(fileToWatch => { if (fileToWatch.path) { // Ensure path is defined setupWatcherForFile(fileToWatch.path, fileToWatch.type); } }); const stop = () => { logFilesToWatch.forEach(fileToWatch => { if (fileToWatch.path) { // Ensure path is defined before unwatching try { fs.unwatchFile(fileToWatch.path); } catch (unwatchError) { // logger.printLine(`Error unwatching ${fileToWatch.path}: ${(unwatchError as Error).message}`, 'debug'); } } }); }; return { stop }; } async save() { await this.saveConfig(); console.log(chalk_1.default.green(`${figures_1.default.tick} Process configuration saved`)); } async stopAll() { const promises = Array.from(this.processes.keys()).map(id => this.stop(id)); await Promise.all(promises); } async restartAll() { const promises = Array.from(this.processes.keys()).map(id => this.restart(id)); await Promise.all(promises); } async deleteAll() { await this.stopAll(); this.processes.clear(); await this.saveConfig(); console.log(chalk_1.default.green(`${figures_1.default.tick} All processes deleted`)); } getProcess(id) { return this.processes.get(id); } async dispose() { if (this.isDisposed) return; this.isDisposed = true; this.stopMonitoring(); // Close all log streams for (const id of this.logStreams.keys()) { this.closeLogStreams(id); } // Stop all processes await this.stopAll(); } } exports.ProcessManager = ProcessManager;