#!/usr/bin/env node /** * Shattered Void MMO Server Shutdown Script * Gracefully stops all running game services */ const { spawn, exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); // Console colors for better output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', }; function log(level, message, data = {}) { const timestamp = new Date().toISOString(); const logData = Object.keys(data).length > 0 ? ` ${JSON.stringify(data, null, 2)}` : ''; let color = colors.white; let levelStr = level.toUpperCase().padEnd(7); switch (level.toLowerCase()) { case 'info': color = colors.cyan; break; case 'success': color = colors.green; break; case 'warn': color = colors.yellow; break; case 'error': color = colors.red; break; } console.log(`${colors.bright}[${timestamp}] [PID:${process.pid}] [${color}${levelStr}${colors.reset}${colors.bright}]${colors.reset} ${color}${message}${colors.reset}${logData}`); } function displayHeader() { console.log(`${colors.cyan}╔═══════════════════════════════════════════════════════════════╗ ║ ║ ║ ${colors.bright}SHATTERED VOID MMO SHUTDOWN${colors.reset}${colors.cyan} ║ ║ ${colors.white}Post-Collapse Galaxy Strategy Game${colors.reset}${colors.cyan} ║ ║ ║ ║ ${colors.yellow}Gracefully stopping all running services...${colors.reset}${colors.cyan} ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); console.log(); } async function findProcesses() { log('info', 'Scanning for running game processes...'); const processes = []; try { // Look for the main startup script const { stdout: startupProcs } = await execAsync('ps aux | grep "node.*start-game.js" | grep -v grep || true'); if (startupProcs.trim()) { const lines = startupProcs.trim().split('\n'); for (const line of lines) { const parts = line.trim().split(/\s+/); const pid = parts[1]; processes.push({ pid, command: 'start-game.js', type: 'main', description: 'Main startup orchestrator' }); } } // Look for Node.js processes on our ports const { stdout: nodeProcs } = await execAsync('ps aux | grep "node" | grep -E "(3000|5173)" | grep -v grep || true'); if (nodeProcs.trim()) { const lines = nodeProcs.trim().split('\n'); for (const line of lines) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (!processes.find(p => p.pid === pid)) { processes.push({ pid, command: 'node (port 3000/5173)', type: 'server', description: 'Backend/Frontend server' }); } } } // Look for npm processes const { stdout: npmProcs } = await execAsync('ps aux | grep "npm.*dev" | grep -v grep || true'); if (npmProcs.trim()) { const lines = npmProcs.trim().split('\n'); for (const line of lines) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (!processes.find(p => p.pid === pid)) { processes.push({ pid, command: 'npm dev', type: 'dev', description: 'NPM development server' }); } } } // Look for Vite processes const { stdout: viteProcs } = await execAsync('ps aux | grep "vite" | grep -v grep || true'); if (viteProcs.trim()) { const lines = viteProcs.trim().split('\n'); for (const line of lines) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (!processes.find(p => p.pid === pid)) { processes.push({ pid, command: 'vite', type: 'frontend', description: 'Vite development server' }); } } } // Look for Python servers (static file serving) const { stdout: pythonProcs } = await execAsync('ps aux | grep "python.*http.server" | grep -v grep || true'); if (pythonProcs.trim()) { const lines = pythonProcs.trim().split('\n'); for (const line of lines) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (!processes.find(p => p.pid === pid)) { processes.push({ pid, command: 'python http.server', type: 'static', description: 'Static file server' }); } } } } catch (error) { log('warn', 'Error scanning for processes:', { error: error.message }); } return processes; } async function checkPorts() { log('info', 'Checking port usage...'); const ports = []; try { const { stdout } = await execAsync('ss -tlnp | grep ":3000\\|:5173" || true'); if (stdout.trim()) { const lines = stdout.trim().split('\n'); for (const line of lines) { const match = line.match(/:(\d+)\s.*users:\(\("([^"]+)",pid=(\d+)/); if (match) { const [, port, process, pid] = match; ports.push({ port, process, pid }); } } } } catch (error) { log('warn', 'Error checking ports:', { error: error.message }); } return ports; } async function stopProcess(process) { log('info', `Stopping ${process.description}`, { pid: process.pid, command: process.command }); try { // Try graceful shutdown first (SIGTERM) await execAsync(`kill -TERM ${process.pid}`); // Wait a moment for graceful shutdown await new Promise(resolve => setTimeout(resolve, 2000)); // Check if process is still running try { await execAsync(`kill -0 ${process.pid}`); log('warn', `Process ${process.pid} still running, forcing shutdown...`); await execAsync(`kill -KILL ${process.pid}`); } catch (error) { // Process already stopped } log('success', `Stopped ${process.description}`, { pid: process.pid }); return true; } catch (error) { if (error.message.includes('No such process')) { log('info', `Process ${process.pid} already stopped`); return true; } log('error', `Failed to stop process ${process.pid}:`, { error: error.message }); return false; } } async function verifyShutdown() { log('info', 'Verifying complete shutdown...'); const remainingProcesses = await findProcesses(); const remainingPorts = await checkPorts(); if (remainingProcesses.length === 0 && remainingPorts.length === 0) { log('success', '✅ All game services successfully stopped'); return true; } else { if (remainingProcesses.length > 0) { log('warn', `${remainingProcesses.length} processes still running:`, { processes: remainingProcesses.map(p => ({ pid: p.pid, command: p.command })) }); } if (remainingPorts.length > 0) { log('warn', `${remainingPorts.length} ports still in use:`, { ports: remainingPorts.map(p => ({ port: p.port, process: p.process, pid: p.pid })) }); } return false; } } async function main() { const startTime = Date.now(); try { displayHeader(); // Phase 1: Discovery log('info', 'Phase 1: Discovering running services'); const processes = await findProcesses(); const ports = await checkPorts(); if (processes.length === 0 && ports.length === 0) { log('info', '🎯 No running game services found'); log('success', '✅ System is already clean'); return; } log('info', `Found ${processes.length} processes and ${ports.length} active ports`); if (processes.length > 0) { console.log('\n📋 Processes to stop:'); processes.forEach(proc => { console.log(` • ${colors.yellow}${proc.description}${colors.reset} (PID: ${proc.pid}) - ${proc.command}`); }); } if (ports.length > 0) { console.log('\n🔌 Ports to free:'); ports.forEach(port => { console.log(` • Port ${colors.cyan}${port.port}${colors.reset} used by ${port.process} (PID: ${port.pid})`); }); } console.log(); // Phase 2: Graceful shutdown log('info', 'Phase 2: Graceful service shutdown'); let stopCount = 0; let failCount = 0; // Stop processes in order of importance (main process first) const processOrder = ['main', 'server', 'dev', 'frontend', 'static']; for (const type of processOrder) { const processesOfType = processes.filter(p => p.type === type); for (const process of processesOfType) { const success = await stopProcess(process); if (success) { stopCount++; } else { failCount++; } } } // Phase 3: Verification log('info', 'Phase 3: Verification and cleanup'); const cleanShutdown = await verifyShutdown(); // Final summary const duration = Date.now() - startTime; console.log(); if (cleanShutdown) { console.log(`${colors.green}╔═══════════════════════════════════════════════════════════════╗ ║ SHUTDOWN COMPLETE ║ ╠═══════════════════════════════════════════════════════════════╣${colors.reset} ${colors.white}║ Duration: ${duration}ms${' '.repeat(50 - duration.toString().length)}║ ║ ║${colors.reset} ${colors.cyan}║ Services Stopped: ║${colors.reset} ${colors.white}║ ✅ All processes terminated ║ ║ ✅ All ports freed ║ ║ ✅ System clean ║${colors.reset} ${colors.green}║ ║ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); log('info', '🎮 Game services stopped successfully'); log('info', '💡 Run "node start-game.js" to restart the game'); } else { console.log(`${colors.yellow}╔═══════════════════════════════════════════════════════════════╗ ║ SHUTDOWN INCOMPLETE ║ ╠═══════════════════════════════════════════════════════════════╣${colors.reset} ${colors.white}║ Duration: ${duration}ms${' '.repeat(50 - duration.toString().length)}║ ║ ║${colors.reset} ${colors.white}║ Stopped: ${stopCount} processes${' '.repeat(42 - stopCount.toString().length)}║ ║ Failed: ${failCount} processes${' '.repeat(43 - failCount.toString().length)}║${colors.reset} ${colors.yellow}║ ║ ║ Some services may still be running. ║ ║ Check the warnings above for details. ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); log('warn', '⚠️ Some services may still be running'); log('info', '💡 You may need to manually stop remaining processes'); process.exit(1); } } catch (error) { const duration = Date.now() - startTime; log('error', 'Shutdown script failed:', { error: error.message, stack: error.stack, duration: `${duration}ms` }); console.log(`${colors.red}╔═══════════════════════════════════════════════════════════════╗ ║ SHUTDOWN FAILED ║ ╠═══════════════════════════════════════════════════════════════╣${colors.reset} ${colors.white}║ Duration: ${duration}ms${' '.repeat(50 - duration.toString().length)}║ ║ ║${colors.reset} ${colors.red}║ An error occurred during shutdown. ║ ║ Some services may still be running. ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); process.exit(1); } } // Handle script interruption process.on('SIGINT', () => { log('warn', 'Shutdown script interrupted'); process.exit(1); }); process.on('SIGTERM', () => { log('warn', 'Shutdown script terminated'); process.exit(1); }); // Run the shutdown script if (require.main === module) { main(); } module.exports = { main };