From e8bd8da3c70e7e54c7f093e3a41312da99f5e1fc Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 3 Mar 2026 23:43:26 +0000 Subject: [PATCH] fix(lifecycle): use ProcessLifecycle for coordinated shutdown Replace per-Watcher SIGINT handlers with a single ProcessLifecycle.install() in TsWatch.start(). This eliminates competing signal handler races that left orphaned child processes. Add @push.rocks/smartexit as direct dependency. --- package.json | 3 ++- pnpm-lock.yaml | 36 ++++++++++++++++++----------------- ts/tswatch.classes.tswatch.ts | 8 ++++++++ ts/tswatch.classes.watcher.ts | 27 +++----------------------- ts/tswatch.plugins.ts | 2 ++ 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 254ec2e..a0aa71a 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,12 @@ "@push.rocks/npmextra": "^5.3.3", "@push.rocks/smartcli": "^4.0.20", "@push.rocks/smartdelay": "^3.0.5", + "@push.rocks/smartexit": "2.0.0", "@push.rocks/smartfs": "^1.3.1", "@push.rocks/smartinteract": "^2.0.16", "@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog-destination-local": "^9.0.2", - "@push.rocks/smartshell": "^3.3.2", + "@push.rocks/smartshell": "^3.3.3", "@push.rocks/smartwatch": "^6.3.0", "@push.rocks/taskbuffer": "^4.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f452ad..80e9690 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@push.rocks/smartdelay': specifier: ^3.0.5 version: 3.0.5 + '@push.rocks/smartexit': + specifier: 2.0.0 + version: 2.0.0 '@push.rocks/smartfs': specifier: ^1.3.1 version: 1.3.1 @@ -45,8 +48,8 @@ importers: specifier: ^9.0.2 version: 9.0.2 '@push.rocks/smartshell': - specifier: ^3.3.2 - version: 3.3.2 + specifier: ^3.3.3 + version: 3.3.3 '@push.rocks/smartwatch': specifier: ^6.3.0 version: 6.3.0 @@ -972,12 +975,12 @@ packages: '@push.rocks/smarterror@2.0.1': resolution: {integrity: sha512-iCcH1D8tlDJgMFsaJ6lhdOTKhbU0KoprNv9MRP9o7691QOx4JEDXiHtr/lNtxVo8BUtdb9CF6kazaknO9KuORA==} - '@push.rocks/smartexit@1.1.0': - resolution: {integrity: sha512-GD8VLIbxQuwvhPXwK4eH162XAYSj+M3wGKWGNO3i1iY4bj8P3BARcgsWx6/ntN3aCo5ygWtrevrfD5iecYY2Ng==} - '@push.rocks/smartexit@1.1.1': resolution: {integrity: sha512-UwcVJbp7vzzDM9RQmnfTaVOJ+DK127lAC5gwyfKU2GfPAv0Jng62Sv601otP+jnly9nRt5fUuttNHDl34Mjn3g==} + '@push.rocks/smartexit@2.0.0': + resolution: {integrity: sha512-gFYW5OWSJCYqSi5H6oEc6d0/cTG4tVC1qMinKXxVjtX7ArlQuDJdvA8Yp4x/mXdDjst1SjkuAzUzE1SIf+S+jg==} + '@push.rocks/smartexpect@2.5.0': resolution: {integrity: sha512-yoyuCoQ3tTiAriuvF+/09fNbVfFnacudL2SwHSzPhX/ugaE7VTSWXQ9A34eKOWvil0MPyDcOY36fVZDxvrPd8A==} @@ -1098,8 +1101,8 @@ packages: '@push.rocks/smartserve@2.0.1': resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==} - '@push.rocks/smartshell@3.3.2': - resolution: {integrity: sha512-xDakRUYBO/WDXlBvS2IbreAvXke/oUul2hcna953a1Bv5gMPOSVBVFsFIaUEqTzAQ5/1YjjEhbnjPeXq87jgkA==} + '@push.rocks/smartshell@3.3.3': + resolution: {integrity: sha512-o7+JWIF3AML5AsHhOo9xMblV2oBZrc9HF/67q42+Xox7/Zw2McQbavXQ2JgsMxUQTc8IRBAWth+LxoFMRqrj4g==} '@push.rocks/smartsitemap@2.0.4': resolution: {integrity: sha512-76dYWG/o/EjV4vYCK7ZKM35T9xgrI+oHEiiIE6E2MDaFIU6QnSfciTfbscH5nc0vxx8Ah+I0HPEJO94BM2S39w==} @@ -5627,7 +5630,7 @@ snapshots: '@push.rocks/smartnpm': 2.0.6 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smartshell': 3.3.2 + '@push.rocks/smartshell': 3.3.3 transitivePeerDependencies: - '@nuxt/kit' - aws-crt @@ -5640,7 +5643,7 @@ snapshots: '@git.zone/tsrun@2.0.1': dependencies: '@push.rocks/smartfile': 13.1.2 - '@push.rocks/smartshell': 3.3.2 + '@push.rocks/smartshell': 3.3.3 tsx: 4.21.0 '@git.zone/tstest@3.1.8(@aws-sdk/credential-providers@3.855.0)(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)': @@ -5665,7 +5668,7 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 5.0.1 '@push.rocks/smarts3': 3.0.3 - '@push.rocks/smartshell': 3.3.2 + '@push.rocks/smartshell': 3.3.3 '@push.rocks/smarttime': 4.1.1 '@types/ws': 8.18.1 figures: 6.1.0 @@ -6067,7 +6070,7 @@ snapshots: '@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartcache': 1.0.18 '@push.rocks/smartenv': 5.0.13 - '@push.rocks/smartexit': 1.1.0 + '@push.rocks/smartexit': 1.1.1 '@push.rocks/smartfile': 11.2.7 '@push.rocks/smartjson': 5.2.0 '@push.rocks/smartpath': 6.0.0 @@ -6307,17 +6310,16 @@ snapshots: clean-stack: 1.3.0 make-error-cause: 2.3.0 - '@push.rocks/smartexit@1.1.0': + '@push.rocks/smartexit@1.1.1': dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartpromise': 4.2.3 tree-kill: 1.2.2 - '@push.rocks/smartexit@1.1.1': + '@push.rocks/smartexit@2.0.0': dependencies: '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartpromise': 4.2.3 tree-kill: 1.2.2 @@ -6579,7 +6581,7 @@ snapshots: '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': dependencies: '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartshell': 3.3.2 + '@push.rocks/smartshell': 3.3.3 puppeteer: 24.36.0(typescript@5.9.3) tree-kill: 1.2.2 transitivePeerDependencies: @@ -6650,10 +6652,10 @@ snapshots: - bufferutil - utf-8-validate - '@push.rocks/smartshell@3.3.2': + '@push.rocks/smartshell@3.3.3': dependencies: '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartexit': 1.1.1 + '@push.rocks/smartexit': 2.0.0 '@push.rocks/smartpromise': 4.2.3 '@types/which': 3.0.4 tree-kill: 1.2.2 diff --git a/ts/tswatch.classes.tswatch.ts b/ts/tswatch.classes.tswatch.ts index 2c16484..197ad29 100644 --- a/ts/tswatch.classes.tswatch.ts +++ b/ts/tswatch.classes.tswatch.ts @@ -46,6 +46,14 @@ export class TsWatch { public async start() { logger.log('info', 'Starting tswatch with config-driven mode'); + // Install global process lifecycle handlers (SIGINT, SIGTERM, etc.) + // This is the single authority for signal handling — no per-watcher handlers. + plugins.smartexit.ProcessLifecycle.install(); + const exitInstance = new plugins.smartexit.SmartExit({ silent: true }); + exitInstance.addCleanupFunction(async () => { + await this.stop(); + }); + // Start server if configured if (this.config.server?.enabled) { await this.startServer(); diff --git a/ts/tswatch.classes.watcher.ts b/ts/tswatch.classes.watcher.ts index c254080..3e85f8c 100644 --- a/ts/tswatch.classes.watcher.ts +++ b/ts/tswatch.classes.watcher.ts @@ -181,34 +181,13 @@ export class Watcher { } /** - * this method sets up a clean exit strategy + * Sets up timeout-based cleanup if configured. + * Signal handling (SIGINT/SIGTERM) is managed globally by ProcessLifecycle in TsWatch. */ private async setupCleanup() { - // Last-resort synchronous cleanup — 'exit' event cannot await async work. - // By this point, SIGINT handler should have already called stop(). - process.on('exit', () => { - if (this.currentExecution && !this.currentExecution.childProcess.killed) { - const pid = this.currentExecution.childProcess.pid; - if (pid) { - try { - process.kill(pid, 'SIGKILL'); - } catch { - // Process may already be dead - } - } - } - }); - process.on('SIGINT', async () => { - console.log(''); - console.log('ok! got SIGINT We are exiting! Just cleaning up to exit neatly :)'); - await this.stop(); - process.exit(0); - }); - - // handle timeout if (this.options.timeout) { plugins.smartdelay.delayFor(this.options.timeout).then(async () => { - console.log(`timed out afer ${this.options.timeout} milliseconds! exiting!`); + console.log(`timed out after ${this.options.timeout} milliseconds! exiting!`); await this.stop(); process.exit(0); }); diff --git a/ts/tswatch.plugins.ts b/ts/tswatch.plugins.ts index 71111a2..d0ec075 100644 --- a/ts/tswatch.plugins.ts +++ b/ts/tswatch.plugins.ts @@ -16,6 +16,7 @@ import * as lik from '@push.rocks/lik'; import * as npmextra from '@push.rocks/npmextra'; import * as smartcli from '@push.rocks/smartcli'; import * as smartdelay from '@push.rocks/smartdelay'; +import * as smartexit from '@push.rocks/smartexit'; import * as smartfs from '@push.rocks/smartfs'; import * as smartinteract from '@push.rocks/smartinteract'; import * as smartlog from '@push.rocks/smartlog'; @@ -29,6 +30,7 @@ export { npmextra, smartcli, smartdelay, + smartexit, smartfs, smartinteract, smartlog,