@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
372 lines โข 13.9 kB
JavaScript
/**
* 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