#!/usr/bin/env node /** * Shattered Void MMO - Comprehensive Startup Orchestrator * * This script provides a complete startup solution for the Shattered Void MMO, * handling all aspects of system initialization, validation, and monitoring. * * Features: * - Pre-flight system checks * - Database connectivity and migration validation * - Redis connectivity with fallback handling * - Backend and frontend server startup * - Health monitoring and service validation * - Graceful error handling and recovery * - Performance metrics and logging */ const path = require('path'); const { spawn, exec } = require('child_process'); const fs = require('fs').promises; const http = require('http'); const express = require('express'); // Load environment variables require('dotenv').config(); // Import our custom modules const StartupChecks = require('./scripts/startup-checks'); const HealthMonitor = require('./scripts/health-monitor'); const DatabaseValidator = require('./scripts/database-validator'); // Node.js version compatibility checking function getNodeVersion() { const version = process.version; const match = version.match(/^v(\d+)\.(\d+)\.(\d+)/); if (!match) { throw new Error(`Unable to parse Node.js version: ${version}`); } return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10), full: version }; } function isViteCompatible() { const nodeVersion = getNodeVersion(); // Vite 7.x requires Node.js 20+ for crypto.hash() support return nodeVersion.major >= 20; } // Configuration const config = { backend: { port: process.env.PORT || 3000, host: process.env.HOST || '0.0.0.0', script: 'src/server.js', startupTimeout: 30000 }, frontend: { port: process.env.FRONTEND_PORT || 5173, host: process.env.FRONTEND_HOST || '0.0.0.0', directory: './frontend', buildDirectory: './frontend/dist', startupTimeout: 20000 }, database: { checkTimeout: 10000, migrationTimeout: 30000 }, redis: { checkTimeout: 5000, optional: true }, startup: { mode: process.env.NODE_ENV || 'development', enableFrontend: process.env.ENABLE_FRONTEND !== 'false', enableHealthMonitoring: process.env.ENABLE_HEALTH_MONITORING !== 'false', healthCheckInterval: 30000, maxRetries: 3, retryDelay: 2000, frontendFallback: process.env.FRONTEND_FALLBACK !== 'false' } }; // Color codes for console 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' }; // Process tracking const processes = { backend: null, frontend: null }; // Startup state const startupState = { startTime: Date.now(), phase: 'initialization', services: {}, metrics: {} }; /** * Enhanced logging with colors and timestamps */ function log(level, message, data = null) { const timestamp = new Date().toISOString(); const pid = process.pid; let colorCode = colors.white; let prefix = 'INFO'; switch (level) { case 'error': colorCode = colors.red; prefix = 'ERROR'; break; case 'warn': colorCode = colors.yellow; prefix = 'WARN'; break; case 'success': colorCode = colors.green; prefix = 'SUCCESS'; break; case 'info': colorCode = colors.cyan; prefix = 'INFO'; break; case 'debug': colorCode = colors.magenta; prefix = 'DEBUG'; break; } const logMessage = `${colors.bright}[${timestamp}] [PID:${pid}] [${prefix}]${colors.reset} ${colorCode}${message}${colors.reset}`; console.log(logMessage); if (data) { console.log(`${colors.blue}${JSON.stringify(data, null, 2)}${colors.reset}`); } } /** * Display startup banner */ function displayBanner() { const banner = ` ${colors.cyan}╔═══════════════════════════════════════════════════════════════╗ ║ ║ ║ ${colors.bright}SHATTERED VOID MMO STARTUP${colors.reset}${colors.cyan} ║ ║ ${colors.white}Post-Collapse Galaxy Strategy Game${colors.reset}${colors.cyan} ║ ║ ║ ║ ${colors.yellow}Mode:${colors.reset} ${colors.white}${config.startup.mode.toUpperCase()}${colors.reset}${colors.cyan} ║ ║ ${colors.yellow}Backend:${colors.reset} ${colors.white}${config.backend.host}:${config.backend.port}${colors.reset}${colors.cyan} ║ ║ ${colors.yellow}Frontend:${colors.reset} ${colors.white}${config.startup.enableFrontend ? `${config.frontend.host}:${config.frontend.port}` : 'Disabled'}${colors.reset}${colors.cyan} ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝${colors.reset} `; console.log(banner); } /** * Update startup phase */ function updatePhase(phase, details = null) { startupState.phase = phase; log('info', `Starting phase: ${phase}`, details); } /** * Measure execution time */ function measureTime(startTime) { return Date.now() - startTime; } /** * Check if a port is available */ function checkPort(port, host = 'localhost') { return new Promise((resolve) => { const server = require('net').createServer(); server.listen(port, host, () => { server.once('close', () => resolve(true)); server.close(); }); server.on('error', () => resolve(false)); }); } /** * Wait for a service to become available */ function waitForService(host, port, timeout = 10000, retries = 10) { return new Promise((resolve, reject) => { let attempts = 0; const interval = timeout / retries; const check = () => { attempts++; const req = http.request({ hostname: host, port: port, path: '/health', method: 'GET', timeout: 2000 }, (res) => { if (res.statusCode === 200) { resolve(true); } else if (attempts < retries) { setTimeout(check, interval); } else { reject(new Error(`Service not ready after ${attempts} attempts`)); } }); req.on('error', () => { if (attempts < retries) { setTimeout(check, interval); } else { reject(new Error(`Service not reachable after ${attempts} attempts`)); } }); req.end(); }; check(); }); } /** * Spawn a process with enhanced monitoring */ function spawnProcess(command, args, options = {}) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], ...options }); child.stdout.on('data', (data) => { const output = data.toString().trim(); if (output) { log('debug', `[${command}] ${output}`); } }); child.stderr.on('data', (data) => { const output = data.toString().trim(); if (output && !output.includes('ExperimentalWarning')) { log('warn', `[${command}] ${output}`); } }); child.on('error', (error) => { log('error', `Process error for ${command}:`, error); reject(error); }); child.on('exit', (code, signal) => { if (code !== 0 && signal !== 'SIGTERM') { const error = new Error(`Process ${command} exited with code ${code}`); log('error', error.message); reject(error); } }); // Consider the process started if it doesn't exit within a second setTimeout(() => { if (!child.killed) { resolve(child); } }, 1000); }); } /** * Pre-flight system checks */ async function runPreflightChecks() { updatePhase('Pre-flight Checks'); const startTime = Date.now(); try { const checks = new StartupChecks(); const results = await checks.runAllChecks(); const duration = measureTime(startTime); startupState.metrics.preflightDuration = duration; if (results.success) { log('success', `Pre-flight checks completed in ${duration}ms`); startupState.services.preflight = { status: 'healthy', checks: results.checks }; } else { log('error', 'Pre-flight checks failed:', results.failures); throw new Error('Pre-flight validation failed'); } } catch (error) { log('error', 'Pre-flight checks error:', error); throw error; } } /** * Validate database connectivity and run migrations */ async function validateDatabase() { updatePhase('Database Validation'); const startTime = Date.now(); try { const validator = new DatabaseValidator(); const results = await validator.validateDatabase(); const duration = measureTime(startTime); startupState.metrics.databaseDuration = duration; if (results.success) { log('success', `Database validation completed in ${duration}ms`); startupState.services.database = { status: 'healthy', ...results }; } else { // Detailed error logging for database validation failures const errorDetails = { general: results.error, connectivity: results.connectivity?.error || null, migrations: results.migrations?.error || null, schema: results.schema?.error || null, missingTables: results.schema?.missingTables || [], seeds: results.seeds?.error || null, integrity: results.integrity?.error || null }; log("error", "Database validation failed:", errorDetails); if (results.schema && !results.schema.success) { log("error", `Schema validation failed - Missing tables: ${results.schema.missingTables.join(", ")}`); log("info", `Current coverage: ${results.schema.coverage}`); if (results.schema.troubleshooting) { log("info", "Troubleshooting suggestions:"); results.schema.troubleshooting.forEach(tip => log("info", ` - ${tip}`)); } } throw new Error(`Database validation failed: ${JSON.stringify(errorDetails, null, 2)}`); } } catch (error) { log('error', 'Database validation error:', error); throw error; } } /** * Start the backend server */ async function startBackendServer() { updatePhase('Backend Server Startup'); const startTime = Date.now(); try { // Check if port is available const portAvailable = await checkPort(config.backend.port, config.backend.host); if (!portAvailable) { throw new Error(`Backend port ${config.backend.port} is already in use`); } // Start the backend process log('info', `Starting backend server on ${config.backend.host}:${config.backend.port}`); const backendProcess = await spawnProcess('node', [config.backend.script], { env: { ...process.env, NODE_ENV: config.startup.mode } }); processes.backend = backendProcess; // Wait for the server to be ready await waitForService(config.backend.host, config.backend.port, config.backend.startupTimeout); const duration = measureTime(startTime); startupState.metrics.backendDuration = duration; log('success', `Backend server started in ${duration}ms`); startupState.services.backend = { status: 'healthy', port: config.backend.port, pid: backendProcess.pid }; } catch (error) { log('error', 'Backend server startup failed:', error); throw error; } } /** * Serve built frontend using Express static server */ async function serveBuildFrontend() { log('info', 'Starting built frontend static server...'); try { // Check if built frontend exists await fs.access(config.frontend.buildDirectory); // Create Express app for serving static files const app = express(); // Serve static files from build directory app.use(express.static(config.frontend.buildDirectory)); // Handle SPA routing - serve index.html for all non-file requests app.get('*', (req, res) => { res.sendFile(path.join(process.cwd(), config.frontend.buildDirectory, 'index.html')); }); // Start the static server const server = app.listen(config.frontend.port, config.frontend.host, () => { log('success', `Built frontend served on ${config.frontend.host}:${config.frontend.port}`); }); // Store server reference for cleanup processes.frontend = { kill: (signal) => { server.close(); }, pid: process.pid }; return server; } catch (error) { log('error', 'Failed to serve built frontend:', error); throw error; } } /** * Build and start the frontend server */ async function startFrontendServer() { if (!config.startup.enableFrontend) { log('info', 'Frontend disabled by configuration'); return; } updatePhase('Frontend Server Startup'); const startTime = Date.now(); // Check Node.js version compatibility with Vite const nodeVersion = getNodeVersion(); const viteCompatible = isViteCompatible(); log('info', `Node.js version: ${nodeVersion.full}`); if (!viteCompatible) { log('warn', `Node.js ${nodeVersion.full} is not compatible with Vite 7.x (requires Node.js 20+)`); log('warn', 'crypto.hash() function is not available in this Node.js version'); if (config.startup.frontendFallback) { log('info', 'Attempting to serve built frontend as fallback...'); try { await serveBuildFrontend(); const duration = measureTime(startTime); startupState.metrics.frontendDuration = duration; log('success', `Built frontend fallback started in ${duration}ms`); startupState.services.frontend = { status: 'healthy', port: config.frontend.port, mode: 'static', nodeCompatibility: 'fallback' }; return; } catch (fallbackError) { log('error', 'Frontend fallback also failed:', fallbackError); throw new Error(`Both Vite dev server and static fallback failed: ${fallbackError.message}`); } } else { throw new Error(`Node.js ${nodeVersion.full} is incompatible with Vite 7.x. Upgrade to Node.js 20+ or enable fallback mode.`); } } try { // Check if frontend directory exists await fs.access(config.frontend.directory); // Check if port is available const portAvailable = await checkPort(config.frontend.port, config.frontend.host); if (!portAvailable) { throw new Error(`Frontend port ${config.frontend.port} is already in use`); } log('info', `Starting Vite development server on ${config.frontend.host}:${config.frontend.port}`); // Start the frontend development server const frontendProcess = await spawnProcess('npm', ['run', 'dev'], { cwd: config.frontend.directory, env: { ...process.env, PORT: config.frontend.port, HOST: config.frontend.host } }); processes.frontend = frontendProcess; // Wait for the server to be ready await waitForService(config.frontend.host, config.frontend.port, config.frontend.startupTimeout); const duration = measureTime(startTime); startupState.metrics.frontendDuration = duration; log('success', `Vite development server started in ${duration}ms`); startupState.services.frontend = { status: 'healthy', port: config.frontend.port, pid: frontendProcess.pid, mode: 'development', nodeCompatibility: 'compatible' }; } catch (error) { log('error', 'Vite development server startup failed:', error); // Try fallback to built frontend if enabled and we haven't tried it yet if (config.startup.frontendFallback && viteCompatible) { log('warn', 'Attempting to serve built frontend as fallback...'); try { await serveBuildFrontend(); const duration = measureTime(startTime); startupState.metrics.frontendDuration = duration; log('success', `Built frontend fallback started in ${duration}ms`); startupState.services.frontend = { status: 'healthy', port: config.frontend.port, mode: 'static', nodeCompatibility: 'fallback' }; return; } catch (fallbackError) { log('error', 'Frontend fallback also failed:', fallbackError); } } // Frontend failure is not critical if we're running in production mode if (config.startup.mode === 'production') { log('warn', 'Continuing without frontend in production mode'); } else { throw error; } } } /** * Start health monitoring */ async function startHealthMonitoring() { if (!config.startup.enableHealthMonitoring) { log('info', 'Health monitoring disabled by configuration'); return; } updatePhase('Health Monitoring Initialization'); try { const monitor = new HealthMonitor({ services: startupState.services, interval: config.startup.healthCheckInterval, onHealthChange: (service, status) => { log(status === 'healthy' ? 'success' : 'error', `Service ${service} status: ${status}`); } }); await monitor.start(); log('success', 'Health monitoring started'); startupState.services.healthMonitor = { status: 'healthy' }; } catch (error) { log('error', 'Health monitoring startup failed:', error); // Health monitoring failure is not critical } } /** * Display startup summary */ function displayStartupSummary() { const totalDuration = measureTime(startupState.startTime); log('success', `🚀 Shattered Void MMO startup completed in ${totalDuration}ms`); const summary = ` ${colors.green}╔═══════════════════════════════════════════════════════════════╗ ║ STARTUP SUMMARY ║ ╠═══════════════════════════════════════════════════════════════╣${colors.reset} ${colors.white}║ Total Duration: ${totalDuration}ms${' '.repeat(47 - totalDuration.toString().length)}║ ║ ║${colors.reset} ${colors.cyan}║ Services Status: ║${colors.reset}`; console.log(summary); Object.entries(startupState.services).forEach(([service, info]) => { const status = info.status === 'healthy' ? '✅' : '❌'; const serviceName = service.charAt(0).toUpperCase() + service.slice(1); const port = info.port ? `:${info.port}` : ''; let extraInfo = ''; // Add extra info for frontend service if (service === 'frontend' && info.mode) { extraInfo = ` (${info.mode})`; } const totalLength = serviceName.length + port.length + extraInfo.length; const line = `${colors.white}║ ${status} ${serviceName}${port}${extraInfo}${' '.repeat(55 - totalLength)}║${colors.reset}`; console.log(line); }); console.log(`${colors.green}║ ║ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); if (config.startup.enableFrontend && startupState.services.frontend) { log('info', `🌐 Game URL: http://${config.frontend.host}:${config.frontend.port}`); } log('info', `📊 API URL: http://${config.backend.host}:${config.backend.port}`); log('info', `📋 Press Ctrl+C to stop all services`); } /** * Graceful shutdown handler */ function setupGracefulShutdown() { const shutdown = async (signal) => { log('warn', `Received ${signal}. Starting graceful shutdown...`); try { // Stop processes if (processes.frontend) { log('info', 'Stopping frontend server...'); processes.frontend.kill('SIGTERM'); } if (processes.backend) { log('info', 'Stopping backend server...'); processes.backend.kill('SIGTERM'); } // Wait a moment for graceful shutdown await new Promise(resolve => setTimeout(resolve, 2000)); log('success', 'All services stopped successfully'); process.exit(0); } catch (error) { log('error', 'Error during shutdown:', error); process.exit(1); } }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); process.on('unhandledRejection', (reason, promise) => { log('error', 'Unhandled Promise Rejection:', { reason, promise: promise.toString() }); }); process.on('uncaughtException', (error) => { log('error', 'Uncaught Exception:', error); process.exit(1); }); } /** * Main startup function */ async function startGame() { try { displayBanner(); setupGracefulShutdown(); // Run startup sequence await runPreflightChecks(); await validateDatabase(); await startBackendServer(); await startFrontendServer(); await startHealthMonitoring(); displayStartupSummary(); } catch (error) { log('error', '💥 Startup failed:', error); // Cleanup any started processes if (processes.backend) processes.backend.kill('SIGTERM'); if (processes.frontend) processes.frontend.kill('SIGTERM'); process.exit(1); } } // Start the game if this file is run directly if (require.main === module) { startGame(); } module.exports = { startGame, config, startupState };