UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

372 lines โ€ข 13.9 kB
/** * Database Cleanup Manager - Permanent Solution for Orphaned Database Connections * @description Handles detection and cleanup of orphaned database processes to prevent * "unable to open database file" errors in WSL2 and Linux environments. * * Key Features: * - Automatic orphaned process detection * - Safe process termination with escalation * - PID lock file management * - WSL2-specific handling * - Graceful shutdown handlers * * @author Optimizely MCP Server * @version 1.0.0 */ import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { getLogger } from '../logging/Logger.js'; export class DatabaseCleanupManager { logger = getLogger(); lockFilePath; dbPath; options; constructor(options) { this.options = options; this.dbPath = path.resolve(options.dbPath); this.lockFilePath = path.join(options.lockFileDir || path.dirname(this.dbPath), `.${path.basename(this.dbPath)}.lock`); } /** * Main cleanup method - call before opening database */ async cleanup() { try { if (this.options.verbose) { this.logger.debug(`๐Ÿ” Checking for orphaned database connections: ${this.dbPath}`); } // Step 1: Check for orphaned processes const orphanedPids = await this.findOrphanedDatabaseProcesses(); if (orphanedPids.length > 0) { this.logger.warn(`โš ๏ธ Found ${orphanedPids.length} orphaned database connections`); await this.terminateOrphanedProcesses(orphanedPids); } // Step 2: Clean up stale WAL files (critical for WSL2) await this.cleanupWALFiles(); // Step 3: Clean up stale lock files await this.cleanupStaleLockFiles(); // Step 4: Create new lock file await this.createLockFile(); // Step 4: Setup cleanup handlers this.setupCleanupHandlers(); if (this.options.verbose) { this.logger.debug('โœ… Database cleanup completed successfully'); } return true; } catch (error) { this.logger.error({ error: error.message }, 'Database cleanup failed'); if (this.options.forceCleanup) { this.logger.warn('๐Ÿ”ง Force cleanup enabled, attempting aggressive cleanup...'); return await this.forceCleanup(); } return false; } } /** * Find processes holding database file handles */ async findOrphanedDatabaseProcesses() { try { // Check platform for appropriate command const isWindows = process.platform === 'win32'; if (isWindows) { // Windows: Skip process detection for faster startup // Most Windows users don't have orphaned processes this.logger.debug('Windows detected: skipping process detection for faster startup'); return []; } else { // Unix: Use lsof to find processes with database file handles const lsofCommand = `lsof "${this.dbPath}" "${this.dbPath}-wal" "${this.dbPath}-shm" 2>/dev/null || true`; const lsofResult = execSync(lsofCommand, { encoding: 'utf8' }); return this.parseUnixProcesses(lsofResult); } } catch (error) { this.logger.warn({ error: error.message }, 'Could not check for orphaned processes'); return []; } } parseUnixProcesses(lsofResult) { if (!lsofResult.trim()) { return []; } // Parse PIDs from lsof output const pids = []; const lines = lsofResult.split('\n'); for (const line of lines) { if (line.includes('node') || line.includes('npm') || line.includes('tsx')) { const parts = line.split(/\s+/); if (parts.length > 1) { const pid = parseInt(parts[1]); if (!isNaN(pid) && !pids.includes(pid)) { pids.push(pid); } } } } return pids; } parseWindowsProcesses(tasklistResult) { const pids = []; try { const lines = tasklistResult.split('\n'); for (const line of lines) { if (line.includes('node.exe')) { const csvParts = line.split(','); if (csvParts.length > 1) { const pid = parseInt(csvParts[1].replace(/"/g, '')); if (!isNaN(pid) && !pids.includes(pid)) { pids.push(pid); } } } } } catch (error) { this.logger.warn('Could not parse Windows processes'); } return pids; } /** * Safely terminate orphaned processes */ async terminateOrphanedProcesses(pids) { for (const pid of pids) { try { // Check if process is still running const isRunning = this.isProcessRunning(pid); if (!isRunning) { continue; } this.logger.info(`๐Ÿ”ง Terminating orphaned process PID ${pid}...`); // Try graceful termination first try { process.kill(pid, 'SIGTERM'); await this.sleep(2000); // Wait 2 seconds if (this.isProcessRunning(pid)) { // Escalate to SIGKILL if still running this.logger.warn(`โšก Escalating to SIGKILL for PID ${pid}`); process.kill(pid, 'SIGKILL'); await this.sleep(1000); } } catch (killError) { // Process might already be dead or no permission this.logger.warn({ pid, error: killError.message }, 'Could not kill process'); } // Verify termination if (!this.isProcessRunning(pid)) { this.logger.info(`โœ… Successfully terminated PID ${pid}`); } else { this.logger.error(`โš ๏ธ Process PID ${pid} still running after termination attempt`); } } catch (error) { this.logger.warn({ pid, error: error.message }, 'Error terminating process'); } } } /** * Check if process is running */ isProcessRunning(pid) { try { // Send signal 0 to check if process exists process.kill(pid, 0); return true; } catch (error) { return false; } } /** * Clean up stale WAL files that can cause "disk I/O error" */ async cleanupWALFiles() { const walFile = `${this.dbPath}-wal`; const shmFile = `${this.dbPath}-shm`; try { // Remove WAL file if it exists if (fs.existsSync(walFile)) { fs.unlinkSync(walFile); if (this.options.verbose) { this.logger.debug(`๐Ÿงน Removed stale WAL file: ${walFile}`); } } // Remove SHM file if it exists if (fs.existsSync(shmFile)) { fs.unlinkSync(shmFile); if (this.options.verbose) { this.logger.debug(`๐Ÿงน Removed stale SHM file: ${shmFile}`); } } } catch (error) { if (this.options.verbose) { this.logger.warn(`โš ๏ธ Warning: Could not clean WAL files: ${error.message}`); } // Don't fail the entire cleanup if WAL cleanup fails } } /** * Clean up stale lock files */ async cleanupStaleLockFiles() { try { if (!fs.existsSync(this.lockFilePath)) { return; } const lockContent = fs.readFileSync(this.lockFilePath, 'utf8'); const lockData = JSON.parse(lockContent); // Check if the process in the lock file is still running if (!this.isProcessRunning(lockData.pid)) { this.logger.info(`๐Ÿงน Removing stale lock file for dead process ${lockData.pid}`); fs.unlinkSync(this.lockFilePath); } } catch (error) { // Lock file might be corrupted, remove it if (fs.existsSync(this.lockFilePath)) { fs.unlinkSync(this.lockFilePath); } } } /** * Create lock file for current process */ async createLockFile() { const lockData = { pid: process.pid, timestamp: Date.now(), dbPath: this.dbPath, command: process.argv.join(' ') }; fs.writeFileSync(this.lockFilePath, JSON.stringify(lockData, null, 2)); } /** * Setup cleanup handlers for graceful shutdown */ setupCleanupHandlers() { const cleanup = () => { try { if (fs.existsSync(this.lockFilePath)) { fs.unlinkSync(this.lockFilePath); } } catch (error) { // Ignore cleanup errors } }; // Handle various exit scenarios process.on('exit', cleanup); process.on('SIGINT', () => { cleanup(); process.exit(0); }); process.on('SIGTERM', () => { cleanup(); process.exit(0); }); process.on('uncaughtException', (error) => { this.logger.error({ error: error.message }, 'Uncaught exception, cleaning up'); cleanup(); process.exit(1); }); process.on('unhandledRejection', (reason) => { this.logger.error({ reason }, 'Unhandled rejection, cleaning up'); cleanup(); process.exit(1); }); } /** * Force cleanup - more aggressive approach */ async forceCleanup() { try { this.logger.warn('๐Ÿ”จ Attempting force cleanup...'); // Kill all node processes that might be holding database files try { const isWindows = process.platform === 'win32'; if (isWindows) { // Windows: Use taskkill to terminate node processes try { execSync(`taskkill /F /IM node.exe 2>nul`, { stdio: 'ignore' }); } catch (error) { // Ignore errors, some processes might not exist } } else { // Unix: Use pkill execSync(`pkill -f "better-sqlite3" 2>/dev/null || true`); execSync(`pkill -f "cache-sync" 2>/dev/null || true`); execSync(`pkill -f "npm.*cache" 2>/dev/null || true`); } await this.sleep(3000); } catch (error) { // Ignore errors, some processes might not exist } // Remove any remaining lock files try { const isWindows = process.platform === 'win32'; const lockDir = path.dirname(this.dbPath); if (isWindows) { // Windows: Use dir and del commands try { const files = require('fs').readdirSync(lockDir); for (const file of files) { if (file.endsWith('.lock')) { require('fs').unlinkSync(path.join(lockDir, file)); } } } catch (error) { // Ignore errors } } else { // Unix: Use rm with glob pattern const lockPattern = path.join(lockDir, '.*.lock'); execSync(`rm -f ${lockPattern} 2>/dev/null || true`); } } catch (error) { // Ignore errors } this.logger.info('โœ… Force cleanup completed'); return true; } catch (error) { this.logger.error({ error: error.message }, 'Force cleanup failed'); return false; } } /** * Helper method for delays */ async sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Static method for WSL2 detection and path optimization */ static optimizeDatabasePath(originalPath) { // Detect WSL2 environment if (process.env.WSL_DISTRO_NAME) { // If not already in Windows filesystem, suggest Windows path if (!originalPath.startsWith('/mnt/c/')) { const filename = path.basename(originalPath); const optimizedPath = `/mnt/c/temp/optly-cache/${filename}`; // Note: Using static logger instance for static method getLogger().info(`๐Ÿ’ก WSL2 detected - consider using Windows filesystem path: ${optimizedPath}`); getLogger().info(` This provides better file locking behavior in WSL2`); } } return originalPath; } } //# sourceMappingURL=DatabaseCleanupManager.js.map