Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a68711d4aa | |||
| 28a09f63b2 | |||
| 76225c6b9f |
@@ -1,5 +1,13 @@
|
||||
# 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)
|
||||
kill full child process trees and add synchronous exit handler to force-kill remaining child processes
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartexit",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.3",
|
||||
"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,8 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"tree-kill": "^1.2.2"
|
||||
"@push.rocks/smartpromise": "^4.2.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -11,15 +11,9 @@ importers:
|
||||
'@push.rocks/lik':
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@push.rocks/smartdelay':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
'@push.rocks/smartpromise':
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
tree-kill:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^4.0.2
|
||||
@@ -853,24 +847,28 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
|
||||
resolution: {integrity: sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
|
||||
resolution: {integrity: sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
|
||||
resolution: {integrity: sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
|
||||
resolution: {integrity: sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==}
|
||||
@@ -918,21 +916,25 @@ packages:
|
||||
resolution: {integrity: sha512-211/XoBiooGGgUo/NxNpsrzGUXtH1d7g/4+UTtjYtfc8QHwu7ZMHcsqg0wss53fXzn/yyxd0DZ56vBHq52BiFw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rspack/binding-linux-arm64-musl@1.6.7':
|
||||
resolution: {integrity: sha512-0WnqAWz3WPDsXGvOOA++or7cHpoidVsH3FlqNaAfRu6ni6n7ig/s0/jKUB+C5FtXOgmGjAGkZHfFgNHsvZ0FWw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rspack/binding-linux-x64-gnu@1.6.7':
|
||||
resolution: {integrity: sha512-iMrE0Q4IuYpkE0MjpaOVaUDYbQFiCRI9D3EPoXzlXJj4kJSdNheODpHTBVRlWt8Xp7UAoWuIFXCvKFKcSMm3aQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rspack/binding-linux-x64-musl@1.6.7':
|
||||
resolution: {integrity: sha512-e7gKFxpdEQwYGk7lTC/hukTgNtaoAstBXehnZNk4k3kuU6+86WDrkn18Cd949iNqfIPtIG/wIsFNGbkHsH69hQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rspack/binding-wasm32-wasi@1.6.7':
|
||||
resolution: {integrity: sha512-yx88EFdE9RP3hh7VhjjW6uc6wGU0KcpOcZp8T8E/a+X8L98fX0aVrtM1IDbndhmdluIMqGbfJNap2+QqOCY9Mw==}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -187,13 +187,19 @@ export class ProcessLifecycle {
|
||||
});
|
||||
}
|
||||
|
||||
/** Synchronous last-resort: SIGKILL any remaining tracked PIDs. */
|
||||
/** Synchronous last-resort: SIGKILL any remaining tracked process groups. */
|
||||
private handleExit(): void {
|
||||
const instances = ProcessLifecycle.getInstances();
|
||||
let killed = 0;
|
||||
|
||||
for (const instance of instances) {
|
||||
for (const pid of instance.trackedPids) {
|
||||
// Kill entire process group (negative PID) for detached children
|
||||
try {
|
||||
process.kill(-pid, 'SIGKILL');
|
||||
killed++;
|
||||
} catch {
|
||||
// Process group may not exist, try single PID
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
killed++;
|
||||
@@ -202,6 +208,7 @@ export class ProcessLifecycle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (killed > 0 && !this.options.silent) {
|
||||
console.error(`[smartexit] Exit handler: force-killed ${killed} remaining child processes`);
|
||||
|
||||
@@ -56,21 +56,31 @@ export type TProcessSignal =
|
||||
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();
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: use native taskkill for tree kill
|
||||
try {
|
||||
plugins.childProcess.execSync(`taskkill /T /F /PID ${pidArg}`, { stdio: 'ignore' });
|
||||
} catch {
|
||||
// Process already dead
|
||||
}
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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. */
|
||||
/** PIDs tracked independently for tree-killing during shutdown. */
|
||||
public trackedPids = new Set<number>();
|
||||
private options: ISmartExitOptions;
|
||||
|
||||
@@ -108,6 +118,9 @@ export class SmartExit {
|
||||
/** Unregister a child process. */
|
||||
public removeProcess(childProcessArg: plugins.childProcess.ChildProcess) {
|
||||
this.processesToEnd.remove(childProcessArg);
|
||||
if (childProcessArg.pid) {
|
||||
this.trackedPids.delete(childProcessArg.pid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,3 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export { lik, smartpromise };
|
||||
|
||||
// third party scope
|
||||
import * as treeKill from 'tree-kill';
|
||||
|
||||
export {
|
||||
treeKill
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user