BREAKING CHANGE(lifecycle): redesign SmartExit with ProcessLifecycle singleton
SmartExit constructor no longer installs signal handlers. Applications must call ProcessLifecycle.install() explicitly. Split into SmartExit (instance process tracking) and ProcessLifecycle (global signal coordination). Remove @push.rocks/smartdelay dependency.
This commit is contained in:
154
ts/smartexit.classes.smartexit.ts
Normal file
154
ts/smartexit.classes.smartexit.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
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<plugins.childProcess.ChildProcess>();
|
||||
public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise<any>>();
|
||||
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<any>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user