Spaces:
Running
Running
| 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<boolean>} - 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<boolean>} | |
| */ | |
| 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<boolean>} | |
| */ | |
| 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<boolean>} - 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<void>} | |
| */ | |
| 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<boolean>} - 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<number>} | |
| */ | |
| 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<boolean>} | |
| */ | |
| 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<boolean>} | |
| */ | |
| export async function killProcessTree(pid, signal = 'SIGTERM') { | |
| const killer = new ProcessKiller(); | |
| return await killer.killProcessTree(pid, signal); | |
| } |