Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a68711d4aa | |||
| 28a09f63b2 | |||
| 76225c6b9f | |||
| a623ac5fe4 | |||
| 34b9aa4463 |
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-04 - 2.0.3 - fix(smartexit)
|
||||||
|
use native OS methods to kill process trees; remove tree-kill dependency
|
||||||
|
|
||||||
|
- Replaced tree-kill usage with taskkill on Windows and process.kill(-pid) on POSIX.
|
||||||
|
- Removed tree-kill package from dependencies and removed its export from plugins.
|
||||||
|
- Handle ESRCH (no such process/group) as non-error when killing process groups.
|
||||||
|
- Ensure trackedPids is cleared when a child process is removed.
|
||||||
|
|
||||||
## 2026-03-03 - 1.1.1 - fix(shutdown)
|
## 2026-03-03 - 1.1.1 - fix(shutdown)
|
||||||
kill full child process trees and add synchronous exit handler to force-kill remaining child processes
|
kill full child process trees and add synchronous exit handler to force-kill remaining child processes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartexit",
|
"name": "@push.rocks/smartexit",
|
||||||
"version": "2.0.0",
|
"version": "2.0.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.",
|
"description": "A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3"
|
||||||
"tree-kill": "^1.2.2"
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -11,15 +11,9 @@ importers:
|
|||||||
'@push.rocks/lik':
|
'@push.rocks/lik':
|
||||||
specifier: ^6.2.2
|
specifier: ^6.2.2
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
'@push.rocks/smartdelay':
|
|
||||||
specifier: ^3.0.5
|
|
||||||
version: 3.0.5
|
|
||||||
'@push.rocks/smartpromise':
|
'@push.rocks/smartpromise':
|
||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
tree-kill:
|
|
||||||
specifier: ^1.2.2
|
|
||||||
version: 1.2.2
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
@@ -853,24 +847,28 @@ packages:
|
|||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
|
||||||
resolution: {integrity: sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==}
|
resolution: {integrity: sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
|
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
|
||||||
resolution: {integrity: sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==}
|
resolution: {integrity: sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
|
'@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
|
||||||
resolution: {integrity: sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==}
|
resolution: {integrity: sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
|
'@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
|
||||||
resolution: {integrity: sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==}
|
resolution: {integrity: sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==}
|
||||||
@@ -918,21 +916,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-211/XoBiooGGgUo/NxNpsrzGUXtH1d7g/4+UTtjYtfc8QHwu7ZMHcsqg0wss53fXzn/yyxd0DZ56vBHq52BiFw==}
|
resolution: {integrity: sha512-211/XoBiooGGgUo/NxNpsrzGUXtH1d7g/4+UTtjYtfc8QHwu7ZMHcsqg0wss53fXzn/yyxd0DZ56vBHq52BiFw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rspack/binding-linux-arm64-musl@1.6.7':
|
'@rspack/binding-linux-arm64-musl@1.6.7':
|
||||||
resolution: {integrity: sha512-0WnqAWz3WPDsXGvOOA++or7cHpoidVsH3FlqNaAfRu6ni6n7ig/s0/jKUB+C5FtXOgmGjAGkZHfFgNHsvZ0FWw==}
|
resolution: {integrity: sha512-0WnqAWz3WPDsXGvOOA++or7cHpoidVsH3FlqNaAfRu6ni6n7ig/s0/jKUB+C5FtXOgmGjAGkZHfFgNHsvZ0FWw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-gnu@1.6.7':
|
'@rspack/binding-linux-x64-gnu@1.6.7':
|
||||||
resolution: {integrity: sha512-iMrE0Q4IuYpkE0MjpaOVaUDYbQFiCRI9D3EPoXzlXJj4kJSdNheODpHTBVRlWt8Xp7UAoWuIFXCvKFKcSMm3aQ==}
|
resolution: {integrity: sha512-iMrE0Q4IuYpkE0MjpaOVaUDYbQFiCRI9D3EPoXzlXJj4kJSdNheODpHTBVRlWt8Xp7UAoWuIFXCvKFKcSMm3aQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-musl@1.6.7':
|
'@rspack/binding-linux-x64-musl@1.6.7':
|
||||||
resolution: {integrity: sha512-e7gKFxpdEQwYGk7lTC/hukTgNtaoAstBXehnZNk4k3kuU6+86WDrkn18Cd949iNqfIPtIG/wIsFNGbkHsH69hQ==}
|
resolution: {integrity: sha512-e7gKFxpdEQwYGk7lTC/hukTgNtaoAstBXehnZNk4k3kuU6+86WDrkn18Cd949iNqfIPtIG/wIsFNGbkHsH69hQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rspack/binding-wasm32-wasi@1.6.7':
|
'@rspack/binding-wasm32-wasi@1.6.7':
|
||||||
resolution: {integrity: sha512-yx88EFdE9RP3hh7VhjjW6uc6wGU0KcpOcZp8T8E/a+X8L98fX0aVrtM1IDbndhmdluIMqGbfJNap2+QqOCY9Mw==}
|
resolution: {integrity: sha512-yx88EFdE9RP3hh7VhjjW6uc6wGU0KcpOcZp8T8E/a+X8L98fX0aVrtM1IDbndhmdluIMqGbfJNap2+QqOCY9Mw==}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartexit',
|
name: '@push.rocks/smartexit',
|
||||||
version: '1.1.1',
|
version: '2.0.3',
|
||||||
description: 'A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.'
|
description: 'A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,21 +187,24 @@ export class ProcessLifecycle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Synchronous last-resort: SIGKILL any remaining child processes. */
|
/** Synchronous last-resort: SIGKILL any remaining tracked process groups. */
|
||||||
private handleExit(): void {
|
private handleExit(): void {
|
||||||
const instances = ProcessLifecycle.getInstances();
|
const instances = ProcessLifecycle.getInstances();
|
||||||
let killed = 0;
|
let killed = 0;
|
||||||
|
|
||||||
for (const instance of instances) {
|
for (const instance of instances) {
|
||||||
const processes = instance.processesToEnd.getArray();
|
for (const pid of instance.trackedPids) {
|
||||||
for (const child of processes) {
|
// Kill entire process group (negative PID) for detached children
|
||||||
const pid = child.pid;
|
try {
|
||||||
if (pid && !child.killed) {
|
process.kill(-pid, 'SIGKILL');
|
||||||
|
killed++;
|
||||||
|
} catch {
|
||||||
|
// Process group may not exist, try single PID
|
||||||
try {
|
try {
|
||||||
process.kill(pid, 'SIGKILL');
|
process.kill(pid, 'SIGKILL');
|
||||||
killed++;
|
killed++;
|
||||||
} catch {
|
} catch {
|
||||||
// Process may already be dead
|
// Process already dead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,20 +56,32 @@ export type TProcessSignal =
|
|||||||
export class SmartExit {
|
export class SmartExit {
|
||||||
/** Kill an entire process tree by PID. */
|
/** Kill an entire process tree by PID. */
|
||||||
public static async killTreeByPid(pidArg: number, signalArg: TProcessSignal = 'SIGKILL') {
|
public static async killTreeByPid(pidArg: number, signalArg: TProcessSignal = 'SIGKILL') {
|
||||||
const done = plugins.smartpromise.defer();
|
if (process.platform === 'win32') {
|
||||||
plugins.treeKill.default(pidArg, signalArg, (err) => {
|
// Windows: use native taskkill for tree kill
|
||||||
if (err) {
|
try {
|
||||||
done.reject(err);
|
plugins.childProcess.execSync(`taskkill /T /F /PID ${pidArg}`, { stdio: 'ignore' });
|
||||||
} else {
|
} catch {
|
||||||
done.resolve();
|
// Process already dead
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
await done.promise;
|
// POSIX: kill the entire process group via negative PID.
|
||||||
|
// Works even for grandchildren reparented to PID 1.
|
||||||
|
try {
|
||||||
|
process.kill(-pidArg, signalArg);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code !== 'ESRCH') {
|
||||||
|
// ESRCH = no such process/group, already dead — that's fine
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance state
|
// Instance state
|
||||||
public processesToEnd = new plugins.lik.ObjectMap<plugins.childProcess.ChildProcess>();
|
public processesToEnd = new plugins.lik.ObjectMap<plugins.childProcess.ChildProcess>();
|
||||||
public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise<any>>();
|
public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise<any>>();
|
||||||
|
/** PIDs tracked independently for tree-killing during shutdown. */
|
||||||
|
public trackedPids = new Set<number>();
|
||||||
private options: ISmartExitOptions;
|
private options: ISmartExitOptions;
|
||||||
|
|
||||||
private log(message: string, isError = false): void {
|
private log(message: string, isError = false): void {
|
||||||
@@ -93,6 +105,9 @@ export class SmartExit {
|
|||||||
/** Register a child process for cleanup on shutdown. */
|
/** Register a child process for cleanup on shutdown. */
|
||||||
public addProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
public addProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
||||||
this.processesToEnd.add(childProcessArg);
|
this.processesToEnd.add(childProcessArg);
|
||||||
|
if (childProcessArg.pid) {
|
||||||
|
this.trackedPids.add(childProcessArg.pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register an async cleanup function to run on shutdown. */
|
/** Register an async cleanup function to run on shutdown. */
|
||||||
@@ -103,6 +118,9 @@ export class SmartExit {
|
|||||||
/** Unregister a child process. */
|
/** Unregister a child process. */
|
||||||
public removeProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
public removeProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
||||||
this.processesToEnd.remove(childProcessArg);
|
this.processesToEnd.remove(childProcessArg);
|
||||||
|
if (childProcessArg.pid) {
|
||||||
|
this.trackedPids.delete(childProcessArg.pid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,23 +144,25 @@ export class SmartExit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Kill child process trees
|
// Phase 2: Kill ALL tracked process trees by PID.
|
||||||
for (const childProcessArg of processes) {
|
// We use trackedPids (not processesToEnd) because the process object may have
|
||||||
const pid = childProcessArg.pid;
|
// been removed by smartshell's exit handler before shutdown runs.
|
||||||
if (pid && !childProcessArg.killed) {
|
// We do NOT check .killed — the direct child may be dead but grandchildren alive.
|
||||||
try {
|
for (const pid of this.trackedPids) {
|
||||||
await SmartExit.killTreeByPid(pid, 'SIGTERM');
|
try {
|
||||||
} catch {
|
await SmartExit.killTreeByPid(pid, 'SIGTERM');
|
||||||
// SIGTERM failed — force kill
|
|
||||||
try {
|
|
||||||
await SmartExit.killTreeByPid(pid, 'SIGKILL');
|
|
||||||
} catch {
|
|
||||||
// Process may already be dead
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processesKilled++;
|
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 };
|
return { processesKilled, cleanupFunctionsRan };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,3 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
|||||||
|
|
||||||
export { lik, smartpromise };
|
export { lik, smartpromise };
|
||||||
|
|
||||||
// third party scope
|
|
||||||
import * as treeKill from 'tree-kill';
|
|
||||||
|
|
||||||
export {
|
|
||||||
treeKill
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user