UNPKG

neex

Version:

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

480 lines (479 loc) 19.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StartManager = void 0; // src/start-manager.ts - Fixed production start manager const child_process_1 = require("child_process"); const chokidar_1 = require("chokidar"); const logger_manager_js_1 = require("./logger-manager.js"); const chalk_1 = __importDefault(require("chalk")); const figures_1 = __importDefault(require("figures")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const http_1 = __importDefault(require("http")); const lodash_1 = require("lodash"); const os_1 = __importDefault(require("os")); class StartManager { constructor(options) { this.workers = new Map(); this.watcher = null; this.healthServer = null; this.isShuttingDown = false; this.totalRestarts = 0; this.envLoaded = false; this.masterProcess = null; this.options = options; this.startTime = new Date(); this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay); } log(message, level = 'info') { logger_manager_js_1.loggerManager.printLine(message, level); } loadEnvFile() { if (this.envLoaded) return; if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) { try { const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8'); const lines = envContent.split('\n'); let loadedCount = 0; for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const [key, ...values] = trimmed.split('='); if (key && values.length > 0) { const value = values.join('=').trim(); const cleanValue = value.replace(/^["']|["']$/g, ''); if (!process.env[key.trim()]) { process.env[key.trim()] = cleanValue; loadedCount++; } } } } if (this.options.verbose && loadedCount > 0) { this.log(`Loaded ${loadedCount} environment variables from ${this.options.envFile}`); } this.envLoaded = true; } catch (error) { if (this.options.verbose) { this.log(`Failed to load environment file: ${error.message}`, 'warn'); } } } } parseMemoryLimit(limit) { var _a; if (!limit) return undefined; const match = limit.match(/^(\d+)([KMGT]?)$/i); if (!match) return undefined; const value = parseInt(match[1]); const unit = ((_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || ''; const multipliers = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024, T: 1024 * 1024 * 1024 * 1024 }; return value * (multipliers[unit] || 1); } getNodeArgs() { const args = []; if (this.options.memoryLimit) { const memoryBytes = this.parseMemoryLimit(this.options.memoryLimit); if (memoryBytes) { const memoryMB = Math.floor(memoryBytes / (1024 * 1024)); args.push(`--max-old-space-size=${memoryMB}`); } } if (this.options.inspect) { args.push('--inspect'); } if (this.options.inspectBrk) { args.push('--inspect-brk'); } if (this.options.nodeArgs) { args.push(...this.options.nodeArgs.split(' ').filter(arg => arg.trim())); } return args; } async startSingleProcess() { const nodeArgs = this.getNodeArgs(); const port = this.options.port || 8000; const env = { ...process.env, NODE_ENV: process.env.NODE_ENV || 'production', PORT: port.toString(), FORCE_COLOR: this.options.color ? '1' : '0', NODE_OPTIONS: '--no-deprecation' }; this.masterProcess = (0, child_process_1.fork)(this.options.file, [], { cwd: this.options.workingDir, env, execArgv: nodeArgs, silent: false // Let the process handle its own output }); this.masterProcess.on('error', (error) => { this.log(`Process error: ${error.message}`, 'error'); }); this.masterProcess.on('exit', (code, signal) => { if (!this.isShuttingDown && code !== 0 && signal !== 'SIGTERM') { this.log(`Process crashed (code: ${code}, signal: ${signal})`, 'error'); this.totalRestarts++; if (this.totalRestarts < this.options.maxCrashes) { setTimeout(() => { this.startSingleProcess(); }, this.options.restartDelay); } else { this.log(`Max crashes reached (${this.options.maxCrashes}), not restarting`, 'error'); } } }); // Wait for process to be ready await new Promise((resolve, reject) => { const timeout = setTimeout(() => { resolve(); // Don't reject, assume it's ready }, 5000); this.masterProcess.on('message', (message) => { if (message && message.type === 'ready') { clearTimeout(timeout); resolve(); } }); this.masterProcess.on('error', (error) => { clearTimeout(timeout); reject(error); }); this.masterProcess.on('exit', (code) => { clearTimeout(timeout); if (code !== 0) { reject(new Error(`Process exited with code ${code}`)); } else { resolve(); } }); }); if (this.options.verbose) { this.log(`Process started (PID: ${this.masterProcess.pid}, Port: ${port})`); } } startWorker(workerId) { return new Promise((resolve, reject) => { var _a, _b; const nodeArgs = this.getNodeArgs(); const port = this.options.port || 8000; const env = { ...process.env, NODE_ENV: process.env.NODE_ENV || 'production', WORKER_ID: workerId.toString(), PORT: port.toString(), CLUSTER_WORKER: 'true', FORCE_COLOR: this.options.color ? '1' : '0', NODE_OPTIONS: '--no-deprecation' }; const workerProcess = (0, child_process_1.fork)(this.options.file, [], { cwd: this.options.workingDir, env, execArgv: nodeArgs, silent: true }); const workerInfo = { process: workerProcess, pid: workerProcess.pid, restarts: 0, startTime: new Date(), id: workerId, port: port }; this.workers.set(workerId, workerInfo); let isReady = false; const readinessTimeout = setTimeout(() => { if (!isReady) { workerProcess.kill(); reject(new Error(`Worker ${workerId} failed to become ready within 15s.`)); } }, 15000); const cleanupReadinessListeners = () => { var _a; clearTimeout(readinessTimeout); (_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.removeListener('data', onDataForReady); workerProcess.removeListener('message', onMessageForReady); }; const onReady = () => { if (isReady) return; isReady = true; cleanupReadinessListeners(); if (this.options.verbose) { this.log(`Worker ${workerId} is ready (PID: ${workerProcess.pid})`); } resolve(workerInfo); }; const onDataForReady = (data) => { const message = data.toString(); const prefix = chalk_1.default.dim(`[Worker ${workerId}] `); process.stdout.write(prefix + message); if (/listening|ready|running on port|local:/i.test(message)) { onReady(); } }; const onMessageForReady = (message) => { if (message && message.type === 'ready') { onReady(); } }; (_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', onDataForReady); (_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => { const prefix = chalk_1.default.red.dim(`[Worker ${workerId}] `); process.stderr.write(prefix + data.toString()); }); workerProcess.on('message', onMessageForReady); workerProcess.on('error', (error) => { if (!isReady) { cleanupReadinessListeners(); reject(error); } this.log(`Worker ${workerId} error: ${error.message}`, 'error'); }); workerProcess.on('exit', (code, signal) => { if (!isReady) { cleanupReadinessListeners(); reject(new Error(`Worker ${workerId} exited with code ${code} before becoming ready.`)); } else { this.workers.delete(workerId); if (!this.isShuttingDown && code !== 0 && signal !== 'SIGTERM') { this.log(`Worker ${workerId} crashed (code: ${code}, signal: ${signal})`, 'error'); this.restartWorker(workerId); } } }); }); } async restartWorker(workerId) { const workerInfo = this.workers.get(workerId); if (workerInfo) { workerInfo.restarts++; this.totalRestarts++; if (workerInfo.restarts >= this.options.maxCrashes) { this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error'); return; } if (this.options.verbose) { this.log(`Restarting worker ${workerId} (attempt ${workerInfo.restarts})`); } try { workerInfo.process.kill('SIGTERM'); await this.waitForProcessExit(workerInfo.process, 5000); } catch (error) { workerInfo.process.kill('SIGKILL'); } setTimeout(() => { this.startWorker(workerId); }, this.options.restartDelay); } } async waitForProcessExit(process, timeout) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Process exit timeout')); }, timeout); process.on('exit', () => { clearTimeout(timer); resolve(); }); if (process.killed) { clearTimeout(timer); resolve(); } }); } async startCluster() { if (this.options.workers === 1) { // Single process mode await this.startSingleProcess(); return; } // Multi-worker mode this.log(`${chalk_1.default.blue(figures_1.default.play)} Starting production server (${this.options.workers} workers)`); const startPromises = []; for (let i = 0; i < this.options.workers; i++) { startPromises.push(this.startWorker(i + 1)); } try { await Promise.all(startPromises); this.log(`${chalk_1.default.green(figures_1.default.tick)} Server ready on port ${this.options.port || 8000} (${this.workers.size} workers)`); } catch (error) { this.log(`Failed to start some workers: ${error.message}`, 'error'); if (this.workers.size > 0) { this.log(`${chalk_1.default.yellow(figures_1.default.warning)} Server partially ready on port ${this.options.port || 8000} (${this.workers.size} workers)`); } } } setupHealthCheck() { if (!this.options.healthCheck) return; this.healthServer = http_1.default.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (req.url === '/health') { const stats = { status: 'ok', uptime: Date.now() - this.startTime.getTime(), workers: this.workers.size, activeWorkers: Array.from(this.workers.values()).map(w => ({ id: w.id, pid: w.pid, restarts: w.restarts, uptime: Date.now() - w.startTime.getTime() })), totalRestarts: this.totalRestarts, memory: process.memoryUsage(), cpu: os_1.default.loadavg(), port: this.options.port || 8000 }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(stats, null, 2)); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); this.healthServer.listen(this.options.healthPort, () => { if (this.options.verbose) { this.log(`Health endpoint: http://localhost:${this.options.healthPort}/health`); } }); } setupWatcher() { if (!this.options.watch) return; const watchPatterns = [ `${this.options.workingDir}/**/*.js`, `${this.options.workingDir}/**/*.json`, `${this.options.workingDir}/**/*.env*` ]; this.watcher = (0, chokidar_1.watch)(watchPatterns, { ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**', '**/tmp/**'], ignoreInitial: true, atomic: 300, awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100 } }); this.watcher.on('change', (filePath) => { if (this.options.verbose) { this.log(`File changed: ${path_1.default.relative(this.options.workingDir, filePath)}`); } this.debouncedRestart(); }); this.watcher.on('error', (error) => { this.log(`Watcher error: ${error.message}`, 'error'); }); if (this.options.verbose) { this.log('File watching enabled'); } } async restartAll() { if (this.isShuttingDown) return; this.log('Restarting due to file changes...'); if (this.options.workers === 1 && this.masterProcess) { // Single process restart try { this.masterProcess.kill('SIGTERM'); await this.waitForProcessExit(this.masterProcess, 5000); } catch (error) { this.masterProcess.kill('SIGKILL'); } setTimeout(() => { this.startSingleProcess(); }, this.options.restartDelay); } else { // Multi-worker restart const restartPromises = []; for (const [workerId, workerInfo] of this.workers.entries()) { restartPromises.push((async () => { try { workerInfo.process.kill('SIGTERM'); await this.waitForProcessExit(workerInfo.process, 5000); } catch (error) { workerInfo.process.kill('SIGKILL'); } })()); } await Promise.allSettled(restartPromises); setTimeout(() => { this.startCluster(); }, this.options.restartDelay); } } async start() { try { // Load environment variables this.loadEnvFile(); // Set up monitoring and health checks this.setupHealthCheck(); this.setupWatcher(); // Start the application await this.startCluster(); } catch (error) { this.log(`Failed to start server: ${error.message}`, 'error'); throw error; } } async stop() { if (this.isShuttingDown) return; this.isShuttingDown = true; this.log(`${chalk_1.default.yellow('⏹')} Shutting down gracefully...`); if (this.watcher) { await this.watcher.close(); } if (this.healthServer) { this.healthServer.close(); } // Stop single process if (this.masterProcess) { try { this.masterProcess.kill('SIGTERM'); await this.waitForProcessExit(this.masterProcess, this.options.gracefulTimeout); } catch (error) { this.masterProcess.kill('SIGKILL'); } } // Stop workers const shutdownPromises = []; for (const [workerId, workerInfo] of this.workers.entries()) { shutdownPromises.push((async () => { try { workerInfo.process.kill('SIGTERM'); await this.waitForProcessExit(workerInfo.process, this.options.gracefulTimeout); } catch (error) { workerInfo.process.kill('SIGKILL'); } })()); } await Promise.allSettled(shutdownPromises); this.workers.clear(); } } exports.StartManager = StartManager;