remote-rdr / utils /ProcessKiller.js
shiveshnavin's picture
Add process killer
7f36d25
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);
}