Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a623ac5fe4 | |||
| 34b9aa4463 | |||
| da24218bef | |||
| 862c67edbb | |||
| ad8e389ef5 | |||
| 1986b3a421 |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-03 - 1.1.1 - fix(shutdown)
|
||||
kill full child process trees and add synchronous exit handler to force-kill remaining child processes
|
||||
|
||||
- Use tree-kill via SmartExit.killTreeByPid to terminate entire process trees (attempt SIGTERM, fallback to SIGKILL).
|
||||
- Replace previous delayed/process.kill calls with awaitable tree-kill for more reliable termination.
|
||||
- Add a synchronous process.on('exit') handler that force-kills any remaining child processes as a last-resort safety net.
|
||||
- No public API changes; internal robustness/bugfix to shutdown behavior.
|
||||
|
||||
## 2025-12-15 - 1.1.0 - feat(smartexit)
|
||||
Add silent logging option, structured shutdown logs, and killAll return stats
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartexit",
|
||||
"version": "1.1.0",
|
||||
"version": "2.0.1",
|
||||
"private": false,
|
||||
"description": "A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -20,7 +20,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartexit',
|
||||
version: '1.1.0',
|
||||
version: '1.1.1',
|
||||
description: 'A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.'
|
||||
}
|
||||
|
||||
163
ts/index.ts
163
ts/index.ts
@@ -1,159 +1,4 @@
|
||||
import * as plugins from './smartexit.plugins.js';
|
||||
|
||||
export interface ISmartExitOptions {
|
||||
silent?: boolean; // Completely disable logging
|
||||
}
|
||||
|
||||
export type TProcessSignal =
|
||||
| 'SIGHUP' // Hangup detected on controlling terminal or death of controlling process
|
||||
| 'SIGINT' // Interrupt from keyboard
|
||||
| 'SIGQUIT' // Quit from keyboard
|
||||
| 'SIGILL' // Illegal Instruction
|
||||
| 'SIGTRAP' // Trace/breakpoint trap
|
||||
| 'SIGABRT' // Abort signal from abort(3)
|
||||
| 'SIGIOT' // IOT trap. A synonym for SIGABRT
|
||||
| 'SIGBUS' // Bus error (bad memory access)
|
||||
| 'SIGFPE' // Floating-point exception
|
||||
| 'SIGKILL' // Kill signal
|
||||
| 'SIGUSR1' // User-defined signal 1
|
||||
| 'SIGSEGV' // Invalid memory reference
|
||||
| 'SIGUSR2' // User-defined signal 2
|
||||
| 'SIGPIPE' // Broken pipe: write to pipe with no readers
|
||||
| 'SIGALRM' // Timer signal from alarm(2)
|
||||
| 'SIGTERM' // Termination signal
|
||||
| 'SIGCHLD' // Child stopped or terminated
|
||||
| 'SIGCONT' // Continue if stopped
|
||||
| 'SIGSTOP' // Stop process
|
||||
| 'SIGTSTP' // Stop typed at terminal
|
||||
| 'SIGTTIN' // Terminal input for background process
|
||||
| 'SIGTTOU' // Terminal output for background process
|
||||
| 'SIGURG' // Urgent condition on socket
|
||||
| 'SIGXCPU' // CPU time limit exceeded
|
||||
| 'SIGXFSZ' // File size limit exceeded
|
||||
| 'SIGVTALRM' // Virtual alarm clock
|
||||
| 'SIGPROF' // Profiling timer expired
|
||||
| 'SIGWINCH' // Window resize signal
|
||||
| 'SIGPOLL' // Pollable event (Sys V). Synonym for SIGIO
|
||||
| 'SIGIO' // I/O now possible (4.2BSD)
|
||||
| 'SIGPWR' // Power failure (System V)
|
||||
| 'SIGINFO' // Information request (some systems)
|
||||
| 'SIGLOST' // Resource lost (unused on most UNIX systems)
|
||||
| 'SIGSYS' // Bad system call (unused on most UNIX systems)
|
||||
| 'SIGUNUSED'; // Synonym for SIGSYS
|
||||
|
||||
export class SmartExit {
|
||||
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
|
||||
public processesToEnd = new plugins.lik.ObjectMap<plugins.childProcess.ChildProcess>();
|
||||
public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise<any>>();
|
||||
private options: ISmartExitOptions;
|
||||
|
||||
/**
|
||||
* Internal logging helper that respects silent option
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* adds a process to be exited
|
||||
* @param childProcessArg
|
||||
*/
|
||||
public addProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
||||
this.processesToEnd.add(childProcessArg);
|
||||
}
|
||||
|
||||
public addCleanupFunction(cleanupFunctionArg: () => Promise<any>) {
|
||||
this.cleanupFunctions.add(cleanupFunctionArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes a process to be exited
|
||||
*/
|
||||
public removeProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
||||
this.processesToEnd.remove(childProcessArg);
|
||||
}
|
||||
|
||||
public async killAll(): Promise<{ processesKilled: number; cleanupFunctionsRan: number }> {
|
||||
const processes = this.processesToEnd.getArray();
|
||||
const cleanupFuncs = this.cleanupFunctions.getArray();
|
||||
let processesKilled = 0;
|
||||
let cleanupFunctionsRan = 0;
|
||||
|
||||
// Kill child processes
|
||||
if (processes.length > 0) {
|
||||
for (const childProcessArg of processes) {
|
||||
const pid = childProcessArg.pid;
|
||||
if (pid) {
|
||||
plugins.smartdelay.delayFor(10000).then(() => {
|
||||
if (childProcessArg.killed) {
|
||||
return;
|
||||
}
|
||||
process.kill(pid, 'SIGKILL');
|
||||
});
|
||||
process.kill(pid, 'SIGINT');
|
||||
processesKilled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run cleanup functions
|
||||
if (cleanupFuncs.length > 0) {
|
||||
for (const cleanupFunction of cleanupFuncs) {
|
||||
await cleanupFunction();
|
||||
cleanupFunctionsRan++;
|
||||
}
|
||||
}
|
||||
|
||||
return { processesKilled, cleanupFunctionsRan };
|
||||
}
|
||||
|
||||
constructor(optionsArg: ISmartExitOptions = {}) {
|
||||
this.options = optionsArg;
|
||||
|
||||
// do app specific cleaning before exiting
|
||||
process.on('exit', async (code) => {
|
||||
if (code === 0) {
|
||||
const { processesKilled, cleanupFunctionsRan } = await this.killAll();
|
||||
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions`);
|
||||
} else {
|
||||
const { processesKilled, cleanupFunctionsRan } = await this.killAll();
|
||||
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions (exit code: ${code})`, true);
|
||||
}
|
||||
});
|
||||
|
||||
// catch ctrl+c event and exit normally
|
||||
process.on('SIGINT', async () => {
|
||||
const { processesKilled, cleanupFunctionsRan } = await this.killAll();
|
||||
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions`);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// catch uncaught exceptions, trace, then exit normally
|
||||
process.on('uncaughtException', async (err) => {
|
||||
this.log(`Uncaught exception: ${err.message}`, true);
|
||||
const { processesKilled, cleanupFunctionsRan } = await this.killAll();
|
||||
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions (exit code: 1)`, true);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
export { SmartExit } from './smartexit.classes.smartexit.js';
|
||||
export type { ISmartExitOptions, TProcessSignal } from './smartexit.classes.smartexit.js';
|
||||
export { ProcessLifecycle } from './smartexit.classes.lifecycle.js';
|
||||
export type { ILifecycleOptions } from './smartexit.classes.lifecycle.js';
|
||||
|
||||
210
ts/smartexit.classes.lifecycle.ts
Normal file
210
ts/smartexit.classes.lifecycle.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
export interface ILifecycleOptions {
|
||||
/** Which signals to intercept. Default: ['SIGINT', 'SIGTERM'] */
|
||||
signals?: Array<'SIGINT' | 'SIGTERM' | 'SIGHUP'>;
|
||||
/** Handle uncaughtException. Default: true */
|
||||
uncaughtExceptions?: boolean;
|
||||
/** Max time in ms for graceful shutdown before force-kill. Default: 10000 */
|
||||
shutdownTimeoutMs?: number;
|
||||
/** Disable lifecycle logging. Default: false */
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<ILifecycleOptions> = {
|
||||
signals: ['SIGINT', 'SIGTERM'],
|
||||
uncaughtExceptions: true,
|
||||
shutdownTimeoutMs: 10000,
|
||||
silent: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Global process lifecycle manager.
|
||||
*
|
||||
* Call `ProcessLifecycle.install()` ONCE from your application entry point.
|
||||
* Libraries should NEVER call install() — they just create SmartExit instances.
|
||||
*
|
||||
* All SmartExit instances auto-register here. On shutdown, all instances'
|
||||
* cleanup functions run and all tracked process trees are killed.
|
||||
*/
|
||||
export class ProcessLifecycle {
|
||||
// Singleton
|
||||
private static instance: ProcessLifecycle | null = null;
|
||||
|
||||
// Global registry of SmartExit instances (populated by SmartExit constructor)
|
||||
// Using 'any' to avoid circular import — actual type is SmartExit
|
||||
private static readonly registry: Set<any> = new Set();
|
||||
private static exitHandlerInstalled = false;
|
||||
|
||||
private options: Required<ILifecycleOptions>;
|
||||
private shuttingDown = false;
|
||||
private signalCount = 0;
|
||||
|
||||
private constructor(options: Required<ILifecycleOptions>) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install global signal handlers.
|
||||
* Call ONCE from the application entry point. Libraries should NOT call this.
|
||||
* Idempotent — subsequent calls return the existing instance.
|
||||
*/
|
||||
public static install(options: ILifecycleOptions = {}): ProcessLifecycle {
|
||||
if (ProcessLifecycle.instance) {
|
||||
return ProcessLifecycle.instance;
|
||||
}
|
||||
|
||||
const merged = { ...DEFAULT_OPTIONS, ...options };
|
||||
const lifecycle = new ProcessLifecycle(merged);
|
||||
ProcessLifecycle.instance = lifecycle;
|
||||
|
||||
// Install signal handlers
|
||||
for (const signal of merged.signals) {
|
||||
process.on(signal, () => lifecycle.handleSignal(signal));
|
||||
}
|
||||
|
||||
// Install uncaughtException handler
|
||||
if (merged.uncaughtExceptions) {
|
||||
process.on('uncaughtException', (err) => lifecycle.handleUncaughtException(err));
|
||||
}
|
||||
|
||||
// Synchronous exit safety net (single handler covers all instances)
|
||||
if (!ProcessLifecycle.exitHandlerInstalled) {
|
||||
ProcessLifecycle.exitHandlerInstalled = true;
|
||||
process.on('exit', () => lifecycle.handleExit());
|
||||
}
|
||||
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
/** Get the installed lifecycle, or null if not installed. */
|
||||
public static getInstance(): ProcessLifecycle | null {
|
||||
return ProcessLifecycle.instance;
|
||||
}
|
||||
|
||||
/** Check whether global handlers are installed. */
|
||||
public static isInstalled(): boolean {
|
||||
return ProcessLifecycle.instance !== null;
|
||||
}
|
||||
|
||||
// ---- Instance registry ----
|
||||
|
||||
/** Called by SmartExit constructor to auto-register. */
|
||||
public static registerInstance(instance: any): void {
|
||||
ProcessLifecycle.registry.add(instance);
|
||||
}
|
||||
|
||||
/** Remove an instance from the global registry. */
|
||||
public static deregisterInstance(instance: any): void {
|
||||
ProcessLifecycle.registry.delete(instance);
|
||||
}
|
||||
|
||||
/** Get all registered SmartExit instances. */
|
||||
public static getInstances(): any[] {
|
||||
return Array.from(ProcessLifecycle.registry);
|
||||
}
|
||||
|
||||
// ---- Shutdown orchestration ----
|
||||
|
||||
/** Whether a shutdown is currently in progress. */
|
||||
public get isShuttingDown(): boolean {
|
||||
return this.shuttingDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate orderly shutdown of all registered instances.
|
||||
* Safe to call multiple times — only the first call executes.
|
||||
*/
|
||||
public async shutdown(exitCode: number = 0): Promise<void> {
|
||||
if (this.shuttingDown) {
|
||||
return;
|
||||
}
|
||||
this.shuttingDown = true;
|
||||
process.exitCode = exitCode;
|
||||
|
||||
const instances = ProcessLifecycle.getInstances();
|
||||
|
||||
let totalProcessesKilled = 0;
|
||||
let totalCleanupRan = 0;
|
||||
|
||||
// Kill all instances in parallel, each running cleanup then tree-kill
|
||||
const shutdownPromises = instances.map(async (instance) => {
|
||||
try {
|
||||
const result = await instance.killAll();
|
||||
totalProcessesKilled += result.processesKilled;
|
||||
totalCleanupRan += result.cleanupFunctionsRan;
|
||||
} catch (err) {
|
||||
if (!this.options.silent) {
|
||||
console.error(`[smartexit] Error during instance cleanup: ${err}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Race against timeout
|
||||
await Promise.race([
|
||||
Promise.allSettled(shutdownPromises),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, this.options.shutdownTimeoutMs)),
|
||||
]);
|
||||
|
||||
if (!this.options.silent) {
|
||||
console.log(
|
||||
`[smartexit] Shutdown complete: ${totalProcessesKilled} processes killed, ` +
|
||||
`${totalCleanupRan} cleanup functions ran`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Signal handlers ----
|
||||
|
||||
private handleSignal(signal: string): void {
|
||||
this.signalCount++;
|
||||
|
||||
if (this.signalCount >= 2) {
|
||||
if (!this.options.silent) {
|
||||
console.log(`\n[smartexit] Force shutdown (received ${signal} twice)`);
|
||||
}
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.silent) {
|
||||
console.log(`\n[smartexit] Received ${signal}, shutting down...`);
|
||||
}
|
||||
|
||||
this.shutdown(0).then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
private handleUncaughtException(err: Error): void {
|
||||
if (!this.options.silent) {
|
||||
console.error(`[smartexit] Uncaught exception: ${err.message}`);
|
||||
if (err.stack) {
|
||||
console.error(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
this.shutdown(1).then(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
/** Synchronous last-resort: SIGKILL any remaining tracked PIDs. */
|
||||
private handleExit(): void {
|
||||
const instances = ProcessLifecycle.getInstances();
|
||||
let killed = 0;
|
||||
|
||||
for (const instance of instances) {
|
||||
for (const pid of instance.trackedPids) {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
killed++;
|
||||
} catch {
|
||||
// Process already dead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (killed > 0 && !this.options.silent) {
|
||||
console.error(`[smartexit] Exit handler: force-killed ${killed} remaining child processes`);
|
||||
}
|
||||
}
|
||||
}
|
||||
161
ts/smartexit.classes.smartexit.ts
Normal file
161
ts/smartexit.classes.smartexit.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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>>();
|
||||
/** PIDs tracked independently — survives removeProcess() so shutdown can still kill the tree. */
|
||||
public trackedPids = new Set<number>();
|
||||
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);
|
||||
if (childProcessArg.pid) {
|
||||
this.trackedPids.add(childProcessArg.pid);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 ALL tracked process trees by PID.
|
||||
// We use trackedPids (not processesToEnd) because the process object may have
|
||||
// been removed by smartshell's exit handler before shutdown runs.
|
||||
// We do NOT check .killed — the direct child may be dead but grandchildren alive.
|
||||
for (const pid of this.trackedPids) {
|
||||
try {
|
||||
await SmartExit.killTreeByPid(pid, 'SIGTERM');
|
||||
processesKilled++;
|
||||
} catch {
|
||||
// SIGTERM failed — force kill
|
||||
try {
|
||||
await SmartExit.killTreeByPid(pid, 'SIGKILL');
|
||||
processesKilled++;
|
||||
} catch {
|
||||
// Process tree already dead — fine
|
||||
}
|
||||
}
|
||||
}
|
||||
this.trackedPids.clear();
|
||||
|
||||
return { processesKilled, cleanupFunctionsRan };
|
||||
}
|
||||
|
||||
/** Remove this instance from the global ProcessLifecycle registry. */
|
||||
public deregister(): void {
|
||||
ProcessLifecycle.deregisterInstance(this);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,9 @@ export { childProcess };
|
||||
|
||||
// pushrocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export { lik, smartdelay, smartpromise };
|
||||
export { lik, smartpromise };
|
||||
|
||||
// third party scope
|
||||
import * as treeKill from 'tree-kill';
|
||||
|
||||
Reference in New Issue
Block a user