/** * Shattered Void MMO - Comprehensive Startup Checks * * This module performs thorough pre-flight checks to ensure all dependencies, * configurations, and system requirements are met before starting the game. */ const fs = require('fs').promises; const path = require('path'); const { exec } = require('child_process'); const { promisify } = require('util'); const net = require('net'); const execAsync = promisify(exec); class StartupChecks { constructor() { this.checks = []; this.results = {}; } /** * Add a check to the validation suite */ addCheck(name, checkFunction, required = true) { this.checks.push({ name, function: checkFunction, required }); } /** * Run all registered checks */ async runAllChecks() { const startTime = Date.now(); const results = { success: true, checks: {}, failures: [], duration: 0 }; // Register all standard checks this.registerStandardChecks(); console.log(`🔍 Running ${this.checks.length} startup checks...`); for (const check of this.checks) { try { console.log(` ⏳ ${check.name}...`); const checkResult = await check.function(); results.checks[check.name] = { success: true, required: check.required, details: checkResult }; console.log(` ✅ ${check.name}`); } catch (error) { const failure = { name: check.name, required: check.required, error: error.message }; results.checks[check.name] = { success: false, required: check.required, error: error.message }; results.failures.push(failure); if (check.required) { results.success = false; console.log(` ❌ ${check.name}: ${error.message}`); } else { console.log(` ⚠️ ${check.name}: ${error.message} (optional)`); } } } results.duration = Date.now() - startTime; return results; } /** * Register all standard checks */ registerStandardChecks() { // Node.js version check this.addCheck('Node.js Version', this.checkNodeVersion, true); // NPM availability this.addCheck('NPM Availability', this.checkNpmAvailability, true); // Environment configuration this.addCheck('Environment Configuration', this.checkEnvironmentConfig, true); // Required directories this.addCheck('Directory Structure', this.checkDirectoryStructure, true); // Package dependencies this.addCheck('Package Dependencies', this.checkPackageDependencies, true); // Port availability this.addCheck('Port Availability', this.checkPortAvailability, true); // Database configuration this.addCheck('Database Configuration', this.checkDatabaseConfig, true); // Redis configuration this.addCheck('Redis Configuration', this.checkRedisConfig, false); // Log directories this.addCheck('Log Directories', this.checkLogDirectories, true); // Frontend availability this.addCheck('Frontend Dependencies', this.checkFrontendDependencies, false); // Memory availability this.addCheck('System Memory', this.checkSystemMemory, true); // Disk space this.addCheck('Disk Space', this.checkDiskSpace, true); // File permissions this.addCheck('File Permissions', this.checkFilePermissions, true); } /** * Check Node.js version requirements */ async checkNodeVersion() { const requiredMajor = 18; const currentVersion = process.version; const major = parseInt(currentVersion.slice(1).split('.')[0]); if (major < requiredMajor) { throw new Error(`Node.js ${requiredMajor}+ required, found ${currentVersion}`); } return { current: currentVersion, required: `>=${requiredMajor}.0.0`, valid: true }; } /** * Check NPM availability */ async checkNpmAvailability() { try { const { stdout } = await execAsync('npm --version'); const version = stdout.trim(); return { version, available: true }; } catch (error) { throw new Error('NPM not found in PATH'); } } /** * Check environment configuration */ async checkEnvironmentConfig() { const envFile = path.join(process.cwd(), '.env'); const config = { hasEnvFile: false, requiredVars: [], missingVars: [], warnings: [] }; // Check for .env file try { await fs.access(envFile); config.hasEnvFile = true; } catch { config.warnings.push('No .env file found, using defaults'); } // Required environment variables (with defaults) const requiredVars = [ { name: 'NODE_ENV', default: 'development' }, { name: 'PORT', default: '3000' }, { name: 'DB_HOST', default: 'localhost' }, { name: 'DB_PORT', default: '5432' }, { name: 'DB_NAME', default: 'shattered_void_dev' }, { name: 'DB_USER', default: 'postgres' } ]; for (const varConfig of requiredVars) { const value = process.env[varConfig.name]; if (!value) { config.missingVars.push({ name: varConfig.name, default: varConfig.default }); } else { config.requiredVars.push({ name: varConfig.name, value: varConfig.name.includes('PASSWORD') ? '[HIDDEN]' : value }); } } return config; } /** * Check directory structure */ async checkDirectoryStructure() { const requiredDirs = [ 'src', 'src/controllers', 'src/services', 'src/routes', 'src/database', 'src/database/migrations', 'config', 'scripts' ]; const optionalDirs = [ 'frontend', 'frontend/src', 'frontend/dist', 'logs', 'tests' ]; const results = { required: [], optional: [], missing: [] }; // Check required directories for (const dir of requiredDirs) { try { const stats = await fs.stat(dir); if (stats.isDirectory()) { results.required.push(dir); } else { results.missing.push(dir); } } catch { results.missing.push(dir); } } // Check optional directories for (const dir of optionalDirs) { try { const stats = await fs.stat(dir); if (stats.isDirectory()) { results.optional.push(dir); } } catch { // Optional directories are not reported as missing } } if (results.missing.length > 0) { throw new Error(`Missing required directories: ${results.missing.join(', ')}`); } return results; } /** * Check package dependencies */ async checkPackageDependencies() { const packageJsonPath = path.join(process.cwd(), 'package.json'); const nodeModulesPath = path.join(process.cwd(), 'node_modules'); try { // Check package.json exists const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); // Check node_modules exists await fs.access(nodeModulesPath); // Check critical dependencies const criticalDeps = [ 'express', 'pg', 'knex', 'winston', 'dotenv', 'socket.io' ]; const missing = []; for (const dep of criticalDeps) { try { await fs.access(path.join(nodeModulesPath, dep)); } catch { missing.push(dep); } } if (missing.length > 0) { throw new Error(`Missing critical dependencies: ${missing.join(', ')}`); } return { packageJson: packageJson.name, version: packageJson.version, dependencies: Object.keys(packageJson.dependencies || {}).length, devDependencies: Object.keys(packageJson.devDependencies || {}).length, criticalDeps: criticalDeps.length }; } catch (error) { throw new Error(`Package validation failed: ${error.message}`); } } /** * Check port availability */ async checkPortAvailability() { const backendPort = process.env.PORT || 3000; const frontendPort = process.env.FRONTEND_PORT || 5173; const checkPort = (port) => { return new Promise((resolve, reject) => { const server = net.createServer(); server.listen(port, (err) => { if (err) { reject(new Error(`Port ${port} is in use`)); } else { server.close(() => resolve(port)); } }); server.on('error', (err) => { reject(new Error(`Port ${port} is in use`)); }); }); }; const results = { backend: await checkPort(backendPort), frontend: null }; // Only check frontend port if frontend is enabled if (process.env.ENABLE_FRONTEND !== 'false') { try { results.frontend = await checkPort(frontendPort); } catch (error) { // Frontend port check is not critical results.frontendError = error.message; } } return results; } /** * Check database configuration */ async checkDatabaseConfig() { const config = { host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'shattered_void_dev', user: process.env.DB_USER || 'postgres' }; // Check if database connection parameters are reasonable if (!config.host || !config.port || !config.database || !config.user) { throw new Error('Incomplete database configuration'); } // Validate port number const port = parseInt(config.port); if (isNaN(port) || port < 1 || port > 65535) { throw new Error(`Invalid database port: ${config.port}`); } return { host: config.host, port: config.port, database: config.database, user: config.user, configured: true }; } /** * Check Redis configuration (optional) */ async checkRedisConfig() { const config = { host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, enabled: process.env.DISABLE_REDIS !== 'true' }; if (!config.enabled) { return { enabled: false, message: 'Redis disabled by configuration' }; } // Validate port number const port = parseInt(config.port); if (isNaN(port) || port < 1 || port > 65535) { throw new Error(`Invalid Redis port: ${config.port}`); } return { host: config.host, port: config.port, enabled: true }; } /** * Check log directories */ async checkLogDirectories() { const logDir = path.join(process.cwd(), 'logs'); try { // Check if logs directory exists await fs.access(logDir); // Check if it's writable await fs.access(logDir, fs.constants.W_OK); return { directory: logDir, exists: true, writable: true }; } catch { // Create logs directory if it doesn't exist try { await fs.mkdir(logDir, { recursive: true }); return { directory: logDir, exists: true, writable: true, created: true }; } catch (error) { throw new Error(`Cannot create logs directory: ${error.message}`); } } } /** * Check frontend dependencies (optional) */ async checkFrontendDependencies() { const frontendDir = path.join(process.cwd(), 'frontend'); try { // Check if frontend directory exists await fs.access(frontendDir); // Check package.json const packageJsonPath = path.join(frontendDir, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); // Check node_modules const nodeModulesPath = path.join(frontendDir, 'node_modules'); await fs.access(nodeModulesPath); return { directory: frontendDir, name: packageJson.name, version: packageJson.version, dependencies: Object.keys(packageJson.dependencies || {}).length, hasNodeModules: true }; } catch (error) { throw new Error(`Frontend not available: ${error.message}`); } } /** * Check system memory */ async checkSystemMemory() { const totalMemory = require('os').totalmem(); const freeMemory = require('os').freemem(); const usedMemory = totalMemory - freeMemory; const totalGB = totalMemory / (1024 * 1024 * 1024); const freeGB = freeMemory / (1024 * 1024 * 1024); const usedGB = usedMemory / (1024 * 1024 * 1024); // Minimum 1GB free memory recommended if (freeGB < 1) { throw new Error(`Low memory: ${freeGB.toFixed(2)}GB free, 1GB+ recommended`); } return { total: `${totalGB.toFixed(2)}GB`, used: `${usedGB.toFixed(2)}GB`, free: `${freeGB.toFixed(2)}GB`, usage: `${((usedGB / totalGB) * 100).toFixed(1)}%` }; } /** * Check disk space */ async checkDiskSpace() { try { const { stdout } = await execAsync('df -h .'); const lines = stdout.trim().split('\n'); const data = lines[1].split(/\s+/); const size = data[1]; const used = data[2]; const available = data[3]; const usage = data[4]; // Extract numeric percentage const usagePercent = parseInt(usage.replace('%', '')); // Warn if disk usage is over 90% if (usagePercent > 90) { throw new Error(`High disk usage: ${usage} used, <10% available`); } return { size, used, available, usage: `${usagePercent}%` }; } catch (error) { // Fallback for non-Unix systems or when df is not available return { message: 'Disk space check not available on this system', available: true }; } } /** * Check file permissions */ async checkFilePermissions() { const criticalFiles = [ 'src/server.js', 'package.json', 'knexfile.js' ]; const results = { readable: [], unreadable: [] }; for (const file of criticalFiles) { try { await fs.access(file, fs.constants.R_OK); results.readable.push(file); } catch { results.unreadable.push(file); } } if (results.unreadable.length > 0) { throw new Error(`Cannot read critical files: ${results.unreadable.join(', ')}`); } return results; } } module.exports = StartupChecks;