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
298 lines โข 10.8 kB
JavaScript
/**
* Browser Process Guardian
* Monitors and manages browser processes to prevent zombies and resource leaks
*/
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export class BrowserProcessGuardian {
static instance;
browserProcesses = new Map();
maxProcesses = 20;
maxProcessAge = 30 * 60 * 1000; // 30 minutes
maxMemoryPerProcess = 512; // 512 MB per process
killCount = 0;
monitoringInterval = null;
static getInstance() {
if (!BrowserProcessGuardian.instance) {
BrowserProcessGuardian.instance = new BrowserProcessGuardian();
}
return BrowserProcessGuardian.instance;
}
/**
* Start monitoring browser processes
*/
startMonitoring() {
if (this.monitoringInterval) {
return; // Already monitoring
}
console.log('๐ฅ๏ธ Starting browser process monitoring...');
// Monitor every 2 minutes
this.monitoringInterval = setInterval(async () => {
try {
await this.monitorProcesses();
await this.cleanupZombieProcesses();
await this.enforceResourceLimits();
}
catch (error) {
console.error('โ ๏ธ Browser process monitoring error:', error);
}
}, 2 * 60 * 1000);
// Initial cleanup
this.cleanupExistingBrowserProcesses();
}
/**
* Stop monitoring browser processes
*/
stopMonitoring() {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
console.log('๐ Stopped browser process monitoring');
}
}
/**
* Register a new browser process
*/
registerBrowserProcess(pid, sessionId, processName = 'browser') {
const process = {
pid,
sessionId,
createdAt: Date.now(),
lastActivity: Date.now(),
processName,
status: 'active'
};
this.browserProcesses.set(pid, process);
console.log(`๐ Registered browser process ${pid} for session ${sessionId}`);
// Check if we're over the limit
if (this.browserProcesses.size > this.maxProcesses) {
console.warn(`โ ๏ธ Browser process limit exceeded (${this.browserProcesses.size}/${this.maxProcesses})`);
this.cleanupOldestProcesses(5); // Kill 5 oldest processes
}
}
/**
* Update process activity timestamp
*/
updateProcessActivity(pid) {
const process = this.browserProcesses.get(pid);
if (process) {
process.lastActivity = Date.now();
}
}
/**
* Unregister a browser process (normal cleanup)
*/
unregisterBrowserProcess(pid) {
const process = this.browserProcesses.get(pid);
if (process) {
process.status = 'terminated';
this.browserProcesses.delete(pid);
console.log(`โ
Unregistered browser process ${pid}`);
}
}
/**
* Monitor all tracked browser processes
*/
async monitorProcesses() {
const currentProcesses = Array.from(this.browserProcesses.values());
for (const process of currentProcesses) {
try {
const isRunning = await this.isProcessRunning(process.pid);
const memoryMB = await this.getProcessMemoryMB(process.pid);
if (!isRunning) {
process.status = 'zombie';
console.warn(`๐ Detected zombie browser process ${process.pid}`);
}
else {
process.memoryMB = memoryMB;
process.status = 'active';
}
}
catch (error) {
console.warn(`โ ๏ธ Failed to monitor process ${process.pid}:`, error);
process.status = 'zombie';
}
}
}
/**
* Clean up zombie and stale browser processes
*/
async cleanupZombieProcesses() {
const now = Date.now();
const processesToKill = [];
for (const process of this.browserProcesses.values()) {
const age = now - process.lastActivity;
// Mark for cleanup if zombie or too old
if (process.status === 'zombie' || age > this.maxProcessAge) {
processesToKill.push(process);
}
}
if (processesToKill.length > 0) {
console.log(`๐งน Cleaning up ${processesToKill.length} stale browser processes`);
for (const process of processesToKill) {
await this.killProcess(process.pid);
this.browserProcesses.delete(process.pid);
}
}
}
/**
* Enforce resource limits (memory, process count)
*/
async enforceResourceLimits() {
const processesToKill = [];
// Check memory limits
for (const process of this.browserProcesses.values()) {
if (process.memoryMB && process.memoryMB > this.maxMemoryPerProcess) {
console.warn(`๐จ Process ${process.pid} using ${process.memoryMB}MB (limit: ${this.maxMemoryPerProcess}MB)`);
processesToKill.push(process);
}
}
// Kill processes that exceed limits
for (const process of processesToKill) {
await this.killProcess(process.pid);
this.browserProcesses.delete(process.pid);
}
}
/**
* Clean up oldest processes when limit is exceeded
*/
async cleanupOldestProcesses(count) {
const processes = Array.from(this.browserProcesses.values())
.sort((a, b) => a.lastActivity - b.lastActivity)
.slice(0, count);
console.log(`๐งน Cleaning up ${processes.length} oldest browser processes`);
for (const process of processes) {
await this.killProcess(process.pid);
this.browserProcesses.delete(process.pid);
}
}
/**
* Check if a process is still running
*/
async isProcessRunning(pid) {
try {
process.kill(pid, 0); // Signal 0 checks if process exists
return true;
}
catch (error) {
return false;
}
}
/**
* Get process memory usage in MB
*/
async getProcessMemoryMB(pid) {
try {
if (process.platform === 'darwin') {
const { stdout } = await execAsync(`ps -o rss= -p ${pid}`);
const rssKB = parseInt(stdout.trim());
return Math.round(rssKB / 1024); // Convert KB to MB
}
else if (process.platform === 'linux') {
const { stdout } = await execAsync(`ps -o rss= -p ${pid}`);
const rssKB = parseInt(stdout.trim());
return Math.round(rssKB / 1024);
}
else {
// Windows
const { stdout } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV`);
const lines = stdout.split('\\n');
if (lines.length > 1) {
const memStr = lines[1].split(',')[4].replace(/"/g, '').replace(/,/g, '');
const memKB = parseInt(memStr);
return Math.round(memKB / 1024);
}
}
}
catch (error) {
// Process might not exist or permission denied
}
return 0;
}
/**
* Kill a browser process
*/
async killProcess(pid) {
try {
console.log(`๐ Killing browser process ${pid}`);
if (process.platform === 'win32') {
await execAsync(`taskkill /F /T /PID ${pid}`);
}
else {
// First try SIGTERM, then SIGKILL
try {
process.kill(pid, 'SIGTERM');
await this.sleep(2000); // Wait 2 seconds
if (await this.isProcessRunning(pid)) {
process.kill(pid, 'SIGKILL');
}
}
catch (error) {
// Process might already be dead
}
}
this.killCount++;
}
catch (error) {
console.warn(`โ ๏ธ Failed to kill process ${pid}:`, error);
}
}
/**
* Clean up any existing browser processes on startup
*/
async cleanupExistingBrowserProcesses() {
try {
console.log('๐งน Cleaning up existing browser processes...');
if (process.platform === 'darwin' || process.platform === 'linux') {
// Kill chrome/chromium processes
// SAFE: Only kill AI-Debug specific browser processes
await execAsync('pkill -f "ai-debug-browser" || true');
}
else if (process.platform === 'win32') {
// SAFE: Only kill AI-Debug specific browser processes on Windows
await execAsync('wmic process where "CommandLine like \'%ai-debug-browser%\'" delete || echo "No AI-Debug browser processes"');
}
console.log('โ
Existing browser process cleanup completed');
}
catch (error) {
console.warn('โ ๏ธ Browser process cleanup failed:', error);
}
}
/**
* Get process statistics
*/
getProcessStats() {
const processes = Array.from(this.browserProcesses.values());
const activeProcesses = processes.filter(p => p.status === 'active').length;
const zombieProcesses = processes.filter(p => p.status === 'zombie').length;
const totalMemoryMB = processes.reduce((sum, p) => sum + (p.memoryMB || 0), 0);
return {
totalProcesses: processes.length,
activeProcesses,
zombieProcesses,
totalMemoryMB,
processesKilled: this.killCount
};
}
/**
* Force cleanup of all browser processes
*/
async forceCleanupAll() {
console.log('๐จ Force cleanup of all browser processes...');
const processes = Array.from(this.browserProcesses.values());
for (const process of processes) {
await this.killProcess(process.pid);
}
this.browserProcesses.clear();
await this.cleanupExistingBrowserProcesses();
console.log('โ
Force cleanup completed');
}
/**
* Sleep utility
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
//# sourceMappingURL=browser-process-guardian.js.map