import { exec, execSync } from 'child_process'; import { platform } from 'os'; /** * Enhanced process killer utility for handling stubborn processes * Handles cross-platform process tree termination with multiple fallback strategies */ export class ProcessKiller { constructor() { this.isWindows = platform() === 'win32'; this.trackedPids = new Set(); } /** * Track a PID for cleanup * @param {number} pid - Process ID to track */ trackPid(pid) { if (pid) { this.trackedPids.add(pid); } } /** * Kill a process tree with the specified signal * @param {number} pid - Process ID to kill * @param {string} signal - Signal to send (SIGTERM, SIGKILL, etc.) * @returns {Promise} - True if successful */ async killProcessTree(pid, signal = 'SIGTERM') { try { if (this.isWindows) { return await this._killWindowsProcessTree(pid); } else { return await this._killUnixProcessTree(pid, signal); } } catch (e) { if (e.code !== 'ESRCH') { console.error('Failed to kill process tree:', e.message); } return false; } } /** * Windows-specific process tree killing * @param {number} pid - Process ID * @returns {Promise} */ async _killWindowsProcessTree(pid) { return new Promise((resolve) => { exec(`taskkill /pid ${pid} /t /f`, (error) => { if (error && !error.message.includes('not found')) { console.error('Failed to kill Windows process tree:', error.message); resolve(false); } else { console.log(`Windows process tree ${pid} killed`); resolve(true); } }); }); } /** * Unix-specific process tree killing * @param {number} pid - Process ID * @param {string} signal - Signal to send * @returns {Promise} */ async _killUnixProcessTree(pid, signal) { try { // Try to kill the process group first process.kill(-pid, signal); console.log(`Unix process group ${pid} killed with ${signal}`); return true; } catch (e) { if (e.code !== 'ESRCH') { // Try individual process kill as fallback try { process.kill(pid, signal); console.log(`Unix process ${pid} killed with ${signal}`); return true; } catch (e2) { if (e2.code !== 'ESRCH') { console.error('Failed to kill Unix process:', e2.message); } return false; } } return false; } } /** * Verify if a process is actually terminated * @param {number} pid - Process ID to check * @returns {Promise} - True if terminated */ async verifyProcessTerminated(pid) { try { if (this.isWindows) { const result = execSync(`tasklist /fi "pid eq ${pid}"`, { encoding: 'utf8' }); return !result.includes(pid.toString()); } else { // On Unix, sending signal 0 checks if process exists process.kill(pid, 0); return false; // If no error thrown, process still exists } } catch (e) { // If error thrown, process doesn't exist return true; } } /** * Kill any remaining processes matching a pattern * @param {string} processPattern - Pattern to match (e.g., "remotion.*render") * @returns {Promise} */ async killRemainingProcesses(processPattern = "remotion.*render") { try { if (this.isWindows) { await this._killRemainingWindowsProcesses(processPattern); } else { await this._killRemainingUnixProcesses(processPattern); } } catch (e) { console.log('Cleanup attempt completed'); } } /** * Windows-specific remaining process cleanup * @param {string} pattern - Process pattern */ async _killRemainingWindowsProcesses(pattern) { return new Promise((resolve) => { exec('tasklist /fi "imagename eq node.exe" /fo csv | findstr remotion', (error, stdout) => { if (!error && stdout) { console.log('Found remaining remotion processes, attempting cleanup...'); exec('taskkill /f /im node.exe /fi "windowtitle eq *remotion*"', (killError) => { if (!killError) { console.log('Cleaned up remaining remotion processes'); } resolve(); }); } else { resolve(); } }); }); } /** * Unix-specific remaining process cleanup * @param {string} pattern - Process pattern */ async _killRemainingUnixProcesses(pattern) { return new Promise((resolve) => { exec(`pkill -f "${pattern}"`, (error) => { if (!error) { console.log('Cleaned up remaining remotion processes'); } resolve(); }); }); } /** * Comprehensive process termination with multiple strategies * @param {number} pid - Process ID to terminate * @param {Object} options - Termination options * @returns {Promise} - True if successfully terminated */ async terminateProcess(pid, options = {}) { const { gracefulTimeout = 2000, forceTimeout = 1000, processPattern = "remotion.*render", onProgress = () => { } } = options; if (!pid) { onProgress('No PID available'); return false; } try { onProgress(`Attempting to kill process tree with PID: ${pid}`); // Stage 1: Graceful termination await this.killProcessTree(pid, 'SIGTERM'); // Wait and verify await new Promise(resolve => setTimeout(resolve, gracefulTimeout)); const isTerminated = await this.verifyProcessTerminated(pid); if (isTerminated) { onProgress('Process terminated successfully with SIGTERM'); return true; } // Stage 2: Force termination onProgress('Process still running, trying SIGKILL'); await this.killProcessTree(pid, 'SIGKILL'); // Final verification await new Promise(resolve => setTimeout(resolve, forceTimeout)); const isFinallyTerminated = await this.verifyProcessTerminated(pid); if (isFinallyTerminated) { onProgress('Process successfully terminated'); return true; } // Stage 3: Cleanup remaining processes onProgress('Process still exists after SIGKILL, attempting additional cleanup'); await this.killRemainingProcesses(processPattern); return true; // Assume success after cleanup attempt } catch (e) { console.error('Failed to terminate process:', e.message); // Final fallback cleanup await this.killRemainingProcesses(processPattern); return false; } } /** * Clean up all tracked PIDs */ clearTrackedPids() { this.trackedPids.clear(); } /** * Get all tracked PIDs * @returns {Set} */ getTrackedPids() { return new Set(this.trackedPids); } } /** * Default export - singleton instance */ export default new ProcessKiller(); /** * Convenience function for quick process termination * @param {number} pid - Process ID * @param {Object} options - Termination options * @returns {Promise} */ export async function terminateProcess(pid, options = {}) { const killer = new ProcessKiller(); return await killer.terminateProcess(pid, options); } /** * Convenience function for process tree killing * @param {number} pid - Process ID * @param {string} signal - Signal to send * @returns {Promise} */ export async function killProcessTree(pid, signal = 'SIGTERM') { const killer = new ProcessKiller(); return await killer.killProcessTree(pid, signal); }