import * as plugins from './smartexit.plugins.js'; import { ProcessLifecycle } from './smartexit.classes.lifecycle.js'; export interface ISmartExitOptions { /** Completely disable logging for this instance. */ silent?: boolean; } export type TProcessSignal = | 'SIGHUP' | 'SIGINT' | 'SIGQUIT' | 'SIGILL' | 'SIGTRAP' | 'SIGABRT' | 'SIGIOT' | 'SIGBUS' | 'SIGFPE' | 'SIGKILL' | 'SIGUSR1' | 'SIGSEGV' | 'SIGUSR2' | 'SIGPIPE' | 'SIGALRM' | 'SIGTERM' | 'SIGCHLD' | 'SIGCONT' | 'SIGSTOP' | 'SIGTSTP' | 'SIGTTIN' | 'SIGTTOU' | 'SIGURG' | 'SIGXCPU' | 'SIGXFSZ' | 'SIGVTALRM' | 'SIGPROF' | 'SIGWINCH' | 'SIGPOLL' | 'SIGIO' | 'SIGPWR' | 'SIGINFO' | 'SIGLOST' | 'SIGSYS' | 'SIGUNUSED'; /** * SmartExit — process and cleanup tracker. * * Lightweight: the constructor does NOT register signal handlers. * Each instance auto-registers with ProcessLifecycle so that global * shutdown (triggered by ProcessLifecycle.install()) kills all tracked processes. * * Libraries should create instances freely. Only the application entry point * should call `ProcessLifecycle.install()`. */ export class SmartExit { /** Kill an entire process tree by PID. */ public static async killTreeByPid(pidArg: number, signalArg: TProcessSignal = 'SIGKILL') { const done = plugins.smartpromise.defer(); plugins.treeKill.default(pidArg, signalArg, (err) => { if (err) { done.reject(err); } else { done.resolve(); } }); await done.promise; } // Instance state public processesToEnd = new plugins.lik.ObjectMap(); public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise>(); private options: ISmartExitOptions; private log(message: string, isError = false): void { if (this.options.silent) { return; } const prefix = '[smartexit]'; if (isError) { console.error(`${prefix} ${message}`); } else { console.log(`${prefix} ${message}`); } } constructor(optionsArg: ISmartExitOptions = {}) { this.options = optionsArg; // Auto-register with the global ProcessLifecycle registry ProcessLifecycle.registerInstance(this); } /** Register a child process for cleanup on shutdown. */ public addProcess(childProcessArg: plugins.childProcess.ChildProcess) { this.processesToEnd.add(childProcessArg); } /** Register an async cleanup function to run on shutdown. */ public addCleanupFunction(cleanupFunctionArg: () => Promise) { this.cleanupFunctions.add(cleanupFunctionArg); } /** Unregister a child process. */ public removeProcess(childProcessArg: plugins.childProcess.ChildProcess) { this.processesToEnd.remove(childProcessArg); } /** * Run cleanup functions, then kill all tracked process trees. * Called by ProcessLifecycle during global shutdown. * Can also be called manually. */ public async killAll(): Promise<{ processesKilled: number; cleanupFunctionsRan: number }> { const processes = this.processesToEnd.getArray(); const cleanupFuncs = this.cleanupFunctions.getArray(); let processesKilled = 0; let cleanupFunctionsRan = 0; // Phase 1: Run cleanup functions (processes still alive) for (const cleanupFunction of cleanupFuncs) { try { await cleanupFunction(); cleanupFunctionsRan++; } catch (err) { this.log(`Cleanup function failed: ${err}`, true); } } // Phase 2: Kill child process trees for (const childProcessArg of processes) { const pid = childProcessArg.pid; if (pid && !childProcessArg.killed) { try { await SmartExit.killTreeByPid(pid, 'SIGTERM'); } catch { // SIGTERM failed — force kill try { await SmartExit.killTreeByPid(pid, 'SIGKILL'); } catch { // Process may already be dead } } processesKilled++; } } return { processesKilled, cleanupFunctionsRan }; } /** Remove this instance from the global ProcessLifecycle registry. */ public deregister(): void { ProcessLifecycle.deregisterInstance(this); } }