browser-process-manager.ts•5.89 kB
import fs from 'fs';
import path from 'path';
import { log } from './logger.js';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Interface representing a browser process entry
*/
interface BrowserProcess {
pid: number;
startTime: number;
serverInstanceId: string; // Unique ID for this server instance
}
/**
* Manages Chrome browser processes to prevent orphaned processes
*/
export class BrowserProcessManager {
private pidFilePath: string;
private serverInstanceId: string;
/**
* Creates a new BrowserProcessManager
*/
constructor() {
this.pidFilePath = path.join(process.cwd(), '.chrome-pids.json');
// Generate a unique ID for this server instance
this.serverInstanceId = Date.now().toString() + '-' + Math.random().toString(36).substring(2, 15);
log.info(`Initialized BrowserProcessManager with instance ID: ${this.serverInstanceId}`);
}
/**
* Read stored browser processes from file
* @returns Array of browser processes
*/
readProcesses(): BrowserProcess[] {
try {
if (fs.existsSync(this.pidFilePath)) {
const data = fs.readFileSync(this.pidFilePath, 'utf8');
return JSON.parse(data) as BrowserProcess[];
}
} catch (error) {
log.error('Error reading browser processes file:', error);
}
return [];
}
/**
* Save browser processes to file
* @param processes Array of browser processes to save
*/
saveProcesses(processes: BrowserProcess[]): void {
try {
fs.writeFileSync(this.pidFilePath, JSON.stringify(processes, null, 2));
} catch (error) {
log.error('Error saving browser processes file:', error);
}
}
/**
* Register a new browser process
* @param pid Process ID of the browser
*/
registerProcess(pid: number): void {
if (!pid) {
log.warn('Attempted to register invalid PID');
return;
}
log.info(`Registering browser process with PID: ${pid}`);
const processes = this.readProcesses();
// Check if this PID is already registered
const existingIndex = processes.findIndex(p => p.pid === pid);
if (existingIndex >= 0) {
// Update the existing entry
processes[existingIndex] = {
pid,
startTime: Date.now(),
serverInstanceId: this.serverInstanceId
};
} else {
// Add a new entry
processes.push({
pid,
startTime: Date.now(),
serverInstanceId: this.serverInstanceId
});
}
this.saveProcesses(processes);
}
/**
* Unregister a browser process
* @param pid Process ID of the browser to unregister
*/
unregisterProcess(pid: number): void {
if (!pid) {
log.warn('Attempted to unregister invalid PID');
return;
}
log.info(`Unregistering browser process with PID: ${pid}`);
const processes = this.readProcesses();
const filteredProcesses = processes.filter(p => p.pid !== pid);
if (processes.length !== filteredProcesses.length) {
this.saveProcesses(filteredProcesses);
}
}
/**
* Check if a process is still running
* @param pid Process ID to check
* @returns True if the process is running, false otherwise
*/
async isProcessRunning(pid: number): Promise<boolean> {
try {
if (process.platform === 'win32') {
// Windows
const { stdout } = await execAsync(`tasklist /FI "PID eq ${pid}" /NH`);
return stdout.includes(pid.toString());
} else {
// Unix-like (Linux, macOS)
await execAsync(`ps -p ${pid} -o pid=`);
return true;
}
} catch (error) {
// Process not found
return false;
}
}
/**
* Kill a process by its PID
* @param pid Process ID to kill
* @returns True if the process was killed successfully, false otherwise
*/
async killProcess(pid: number): Promise<boolean> {
try {
log.info(`Attempting to kill browser process with PID: ${pid}`);
if (process.platform === 'win32') {
// Windows
await execAsync(`taskkill /F /PID ${pid}`);
} else {
// Unix-like (Linux, macOS)
await execAsync(`kill -9 ${pid}`);
}
return true;
} catch (error) {
log.error(`Failed to kill process ${pid}:`, error);
return false;
}
}
/**
* Clean up orphaned browser processes
*/
async cleanupOrphanedProcesses(): Promise<void> {
log.info('Cleaning up orphaned browser processes...');
const processes = this.readProcesses();
const validProcesses: BrowserProcess[] = [];
for (const process of processes) {
const isRunning = await this.isProcessRunning(process.pid);
if (isRunning) {
// Process is still running, check if it's orphaned
// We consider a process orphaned if it's not from this server instance
// and it's been running for more than 10 minutes
const isFromCurrentInstance = process.serverInstanceId === this.serverInstanceId;
const processAgeMs = Date.now() - process.startTime;
const isOld = processAgeMs > 10 * 60 * 1000; // 10 minutes
if (!isFromCurrentInstance && isOld) {
log.info(`Found orphaned browser process with PID: ${process.pid}`);
const killed = await this.killProcess(process.pid);
if (!killed) {
// If we couldn't kill it, keep it in the list
validProcesses.push(process);
}
} else {
// Process is still valid
validProcesses.push(process);
}
}
// If not running, we don't add it to validProcesses
}
// Save the updated list
this.saveProcesses(validProcesses);
log.info(`Cleanup complete. ${processes.length - validProcesses.length} orphaned processes removed.`);
}
}