diff --git a/changelog.md b/changelog.md index ffce761..52e3810 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/package.json b/package.json index 72e6d63..92d686e 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76fae57..21984cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index f277c0b..6027f72 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/smartexit.classes.smartexit.ts b/ts/smartexit.classes.smartexit.ts index dc37e28..09a3e43 100644 --- a/ts/smartexit.classes.smartexit.ts +++ b/ts/smartexit.classes.smartexit.ts @@ -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 } - }); - await done.promise; + } 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; + } + } + } } // Instance state public processesToEnd = new plugins.lik.ObjectMap(); public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise>(); - /** PIDs tracked independently — survives removeProcess() so shutdown can still kill the tree. */ + /** PIDs tracked independently for tree-killing during shutdown. */ public trackedPids = new Set(); 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); + } } /** diff --git a/ts/smartexit.plugins.ts b/ts/smartexit.plugins.ts index 3cea941..ecc1cc8 100644 --- a/ts/smartexit.plugins.ts +++ b/ts/smartexit.plugins.ts @@ -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 -}