UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

284 lines • 10.9 kB
/** * Cleanup Coordinator - Project-Scoped Resource Cleanup * * Manages temporary directories and ensures clean shutdown of project resources * with isolation between different debugging sessions. */ import * as fs from 'fs/promises'; import { existsSync } from 'fs'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export class CleanupCoordinator { tempBaseDirectory; projectTempDirs; constructor(tempBaseDirectory = '/tmp/ai-debug-sessions') { this.tempBaseDirectory = tempBaseDirectory; this.projectTempDirs = new Map(); } /** * Create isolated temporary directory for a project session */ async createProjectTempDir(projectId) { // Check if already exists const existing = this.projectTempDirs.get(projectId); if (existing && existsSync(existing)) { return existing; } // Ensure base directory exists await this.ensureBaseDirectory(); // Create project-specific directory const projectTempDir = path.join(this.tempBaseDirectory, projectId); try { await fs.mkdir(projectTempDir, { recursive: true }); // Create subdirectories for different types of temp files await Promise.all([ fs.mkdir(path.join(projectTempDir, 'browser-data'), { recursive: true }), fs.mkdir(path.join(projectTempDir, 'screenshots'), { recursive: true }), fs.mkdir(path.join(projectTempDir, 'logs'), { recursive: true }), fs.mkdir(path.join(projectTempDir, 'reports'), { recursive: true }) ]); this.projectTempDirs.set(projectId, projectTempDir); return projectTempDir; } catch (error) { throw new Error(`Failed to create temp directory for project ${projectId}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get temporary directory for a project */ getProjectTempDir(projectId) { return this.projectTempDirs.get(projectId) || null; } /** * Get specific subdirectory path for a project */ getProjectSubDir(projectId, subDir) { const projectTempDir = this.projectTempDirs.get(projectId); if (!projectTempDir) { return null; } return path.join(projectTempDir, subDir); } /** * Cleanup specific project's temporary files */ async cleanupProject(projectId) { const projectTempDir = this.projectTempDirs.get(projectId); if (!projectTempDir || !existsSync(projectTempDir)) { return; } try { // Kill any browser processes using this temp directory await this.killBrowserProcesses(projectTempDir); // Wait a moment for processes to terminate await this.sleep(1000); // Remove the entire project directory await fs.rm(projectTempDir, { recursive: true, force: true }); // Remove from tracking this.projectTempDirs.delete(projectId); } catch (error) { // If removal fails, mark directory for later cleanup await this.markForDeferredCleanup(projectTempDir); throw new Error(`Failed to cleanup project ${projectId}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Emergency cleanup - remove all temporary directories */ async emergencyCleanup() { try { // Kill all browser processes in temp directories for (const tempDir of this.projectTempDirs.values()) { try { await this.killBrowserProcesses(tempDir); } catch (error) { // Continue with cleanup even if process killing fails } } // Wait for processes to terminate await this.sleep(2000); // Remove entire base directory if (existsSync(this.tempBaseDirectory)) { await fs.rm(this.tempBaseDirectory, { recursive: true, force: true }); } // Clear tracking this.projectTempDirs.clear(); } catch (error) { throw new Error(`Emergency cleanup failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Cleanup old/abandoned temporary directories */ async cleanupAbandonedDirs() { const cleanedDirs = []; try { if (!existsSync(this.tempBaseDirectory)) { return cleanedDirs; } const entries = await fs.readdir(this.tempBaseDirectory, { withFileTypes: true }); const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours for (const entry of entries) { if (entry.isDirectory()) { const dirPath = path.join(this.tempBaseDirectory, entry.name); try { const stats = await fs.stat(dirPath); const age = now - stats.mtime.getTime(); // If directory is older than maxAge and not in active tracking if (age > maxAge && !this.projectTempDirs.has(entry.name)) { await this.killBrowserProcesses(dirPath); await this.sleep(1000); await fs.rm(dirPath, { recursive: true, force: true }); cleanedDirs.push(entry.name); } } catch (error) { // Skip directories that can't be processed continue; } } } } catch (error) { // Return what we managed to clean up } return cleanedDirs; } /** * Get disk usage for all project temp directories */ async getDiskUsage() { const usage = {}; for (const [projectId, tempDir] of this.projectTempDirs) { try { usage[projectId] = await this.getDirectorySize(tempDir); } catch (error) { usage[projectId] = 0; } } return usage; } /** * Get total disk usage for all sessions */ async getTotalDiskUsage() { const usage = await this.getDiskUsage(); return Object.values(usage).reduce((sum, size) => sum + size, 0); } /** * Cleanup projects exceeding disk quota */ async cleanupOverQuota(maxSizeMB) { const usage = await this.getDiskUsage(); const cleanedProjects = []; for (const [projectId, sizeMB] of Object.entries(usage)) { if (sizeMB > maxSizeMB) { try { await this.cleanupProject(projectId); cleanedProjects.push(projectId); } catch (error) { // Continue with other projects } } } return cleanedProjects; } // Private helper methods async ensureBaseDirectory() { if (!existsSync(this.tempBaseDirectory)) { await fs.mkdir(this.tempBaseDirectory, { recursive: true }); } } async killBrowserProcesses(tempDir) { try { // Find and kill processes using this temp directory const { stdout } = await execAsync(`ps aux | grep -E "(chromium|chrome|firefox|webkit)" | grep "${tempDir}" | awk '{print $2}'`); if (stdout.trim()) { const pids = stdout.trim().split('\n').filter(pid => pid); if (pids.length > 0) { // First try SIGTERM await execAsync(`kill ${pids.join(' ')}`).catch(() => { }); await this.sleep(2000); // Then SIGKILL if needed await execAsync(`kill -9 ${pids.join(' ')}`).catch(() => { }); } } } catch (error) { // Process killing is best effort } } async getDirectorySize(dirPath) { try { const { stdout } = await execAsync(`du -sm "${dirPath}" | cut -f1`); return parseInt(stdout.trim()) || 0; } catch (error) { return 0; } } async markForDeferredCleanup(dirPath) { try { // Create a marker file for later cleanup const markerFile = path.join(dirPath, '.cleanup-deferred'); await fs.writeFile(markerFile, new Date().toISOString()); } catch (error) { // Ignore marker file creation errors } } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Validate that temp directories are properly isolated */ async validateIsolation() { const conflicts = []; const recommendations = []; try { // Check for permission issues const testFile = path.join(this.tempBaseDirectory, 'test-write-permissions'); await fs.writeFile(testFile, 'test'); await fs.unlink(testFile); } catch (error) { conflicts.push(`Base directory not writable: ${this.tempBaseDirectory}`); recommendations.push('Check directory permissions or use different temp location'); } // Check for naming conflicts const projectIds = Array.from(this.projectTempDirs.keys()); const duplicates = projectIds.filter((id, index) => projectIds.indexOf(id) !== index); if (duplicates.length > 0) { conflicts.push(`Duplicate project IDs detected: ${duplicates.join(', ')}`); recommendations.push('Ensure project ID generation is unique'); } // Check for abandoned processes try { const { stdout } = await execAsync(`ps aux | grep -E "(chromium|chrome).*${this.tempBaseDirectory}" | grep -v grep | wc -l`); const processCount = parseInt(stdout.trim()) || 0; if (processCount > this.projectTempDirs.size * 3) { // Reasonable process limit per project conflicts.push(`Excessive browser processes detected: ${processCount}`); recommendations.push('Run emergency cleanup to terminate abandoned processes'); } } catch (error) { // Process check is informational } return { isolated: conflicts.length === 0, conflicts, recommendations }; } } //# sourceMappingURL=cleanup-coordinator.js.map