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
JavaScript
/**
* 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