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.
This commit is contained in:
2026-03-03 23:43:26 +00:00
parent 91b3e273de
commit e8bd8da3c7
5 changed files with 34 additions and 42 deletions

View File

@@ -31,11 +31,12 @@
"@push.rocks/npmextra": "^5.3.3", "@push.rocks/npmextra": "^5.3.3",
"@push.rocks/smartcli": "^4.0.20", "@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartexit": "2.0.0",
"@push.rocks/smartfs": "^1.3.1", "@push.rocks/smartfs": "^1.3.1",
"@push.rocks/smartinteract": "^2.0.16", "@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartlog-destination-local": "^9.0.2", "@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/smartwatch": "^6.3.0",
"@push.rocks/taskbuffer": "^4.2.0" "@push.rocks/taskbuffer": "^4.2.0"
}, },

36
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
'@push.rocks/smartdelay': '@push.rocks/smartdelay':
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
'@push.rocks/smartexit':
specifier: 2.0.0
version: 2.0.0
'@push.rocks/smartfs': '@push.rocks/smartfs':
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1 version: 1.3.1
@@ -45,8 +48,8 @@ importers:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
'@push.rocks/smartshell': '@push.rocks/smartshell':
specifier: ^3.3.2 specifier: ^3.3.3
version: 3.3.2 version: 3.3.3
'@push.rocks/smartwatch': '@push.rocks/smartwatch':
specifier: ^6.3.0 specifier: ^6.3.0
version: 6.3.0 version: 6.3.0
@@ -972,12 +975,12 @@ packages:
'@push.rocks/smarterror@2.0.1': '@push.rocks/smarterror@2.0.1':
resolution: {integrity: sha512-iCcH1D8tlDJgMFsaJ6lhdOTKhbU0KoprNv9MRP9o7691QOx4JEDXiHtr/lNtxVo8BUtdb9CF6kazaknO9KuORA==} resolution: {integrity: sha512-iCcH1D8tlDJgMFsaJ6lhdOTKhbU0KoprNv9MRP9o7691QOx4JEDXiHtr/lNtxVo8BUtdb9CF6kazaknO9KuORA==}
'@push.rocks/smartexit@1.1.0':
resolution: {integrity: sha512-GD8VLIbxQuwvhPXwK4eH162XAYSj+M3wGKWGNO3i1iY4bj8P3BARcgsWx6/ntN3aCo5ygWtrevrfD5iecYY2Ng==}
'@push.rocks/smartexit@1.1.1': '@push.rocks/smartexit@1.1.1':
resolution: {integrity: sha512-UwcVJbp7vzzDM9RQmnfTaVOJ+DK127lAC5gwyfKU2GfPAv0Jng62Sv601otP+jnly9nRt5fUuttNHDl34Mjn3g==} 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': '@push.rocks/smartexpect@2.5.0':
resolution: {integrity: sha512-yoyuCoQ3tTiAriuvF+/09fNbVfFnacudL2SwHSzPhX/ugaE7VTSWXQ9A34eKOWvil0MPyDcOY36fVZDxvrPd8A==} resolution: {integrity: sha512-yoyuCoQ3tTiAriuvF+/09fNbVfFnacudL2SwHSzPhX/ugaE7VTSWXQ9A34eKOWvil0MPyDcOY36fVZDxvrPd8A==}
@@ -1098,8 +1101,8 @@ packages:
'@push.rocks/smartserve@2.0.1': '@push.rocks/smartserve@2.0.1':
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==} resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
'@push.rocks/smartshell@3.3.2': '@push.rocks/smartshell@3.3.3':
resolution: {integrity: sha512-xDakRUYBO/WDXlBvS2IbreAvXke/oUul2hcna953a1Bv5gMPOSVBVFsFIaUEqTzAQ5/1YjjEhbnjPeXq87jgkA==} resolution: {integrity: sha512-o7+JWIF3AML5AsHhOo9xMblV2oBZrc9HF/67q42+Xox7/Zw2McQbavXQ2JgsMxUQTc8IRBAWth+LxoFMRqrj4g==}
'@push.rocks/smartsitemap@2.0.4': '@push.rocks/smartsitemap@2.0.4':
resolution: {integrity: sha512-76dYWG/o/EjV4vYCK7ZKM35T9xgrI+oHEiiIE6E2MDaFIU6QnSfciTfbscH5nc0vxx8Ah+I0HPEJO94BM2S39w==} resolution: {integrity: sha512-76dYWG/o/EjV4vYCK7ZKM35T9xgrI+oHEiiIE6E2MDaFIU6QnSfciTfbscH5nc0vxx8Ah+I0HPEJO94BM2S39w==}
@@ -5627,7 +5630,7 @@ snapshots:
'@push.rocks/smartnpm': 2.0.6 '@push.rocks/smartnpm': 2.0.6
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartshell': 3.3.2 '@push.rocks/smartshell': 3.3.3
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- aws-crt - aws-crt
@@ -5640,7 +5643,7 @@ snapshots:
'@git.zone/tsrun@2.0.1': '@git.zone/tsrun@2.0.1':
dependencies: dependencies:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartshell': 3.3.2 '@push.rocks/smartshell': 3.3.3
tsx: 4.21.0 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)': '@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/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smarts3': 3.0.3 '@push.rocks/smarts3': 3.0.3
'@push.rocks/smartshell': 3.3.2 '@push.rocks/smartshell': 3.3.3
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
'@types/ws': 8.18.1 '@types/ws': 8.18.1
figures: 6.1.0 figures: 6.1.0
@@ -6067,7 +6070,7 @@ snapshots:
'@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartbucket': 3.3.10
'@push.rocks/smartcache': 1.0.18 '@push.rocks/smartcache': 1.0.18
'@push.rocks/smartenv': 5.0.13 '@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/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -6307,17 +6310,16 @@ snapshots:
clean-stack: 1.3.0 clean-stack: 1.3.0
make-error-cause: 2.3.0 make-error-cause: 2.3.0
'@push.rocks/smartexit@1.1.0': '@push.rocks/smartexit@1.1.1':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
tree-kill: 1.2.2 tree-kill: 1.2.2
'@push.rocks/smartexit@1.1.1': '@push.rocks/smartexit@2.0.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
tree-kill: 1.2.2 tree-kill: 1.2.2
@@ -6579,7 +6581,7 @@ snapshots:
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@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) puppeteer: 24.36.0(typescript@5.9.3)
tree-kill: 1.2.2 tree-kill: 1.2.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -6650,10 +6652,10 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
'@push.rocks/smartshell@3.3.2': '@push.rocks/smartshell@3.3.3':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartexit': 1.1.1 '@push.rocks/smartexit': 2.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@types/which': 3.0.4 '@types/which': 3.0.4
tree-kill: 1.2.2 tree-kill: 1.2.2

View File

@@ -46,6 +46,14 @@ export class TsWatch {
public async start() { public async start() {
logger.log('info', 'Starting tswatch with config-driven mode'); 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 // Start server if configured
if (this.config.server?.enabled) { if (this.config.server?.enabled) {
await this.startServer(); await this.startServer();

View File

@@ -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() { 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) { if (this.options.timeout) {
plugins.smartdelay.delayFor(this.options.timeout).then(async () => { 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(); await this.stop();
process.exit(0); process.exit(0);
}); });

View File

@@ -16,6 +16,7 @@ import * as lik from '@push.rocks/lik';
import * as npmextra from '@push.rocks/npmextra'; import * as npmextra from '@push.rocks/npmextra';
import * as smartcli from '@push.rocks/smartcli'; import * as smartcli from '@push.rocks/smartcli';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartexit from '@push.rocks/smartexit';
import * as smartfs from '@push.rocks/smartfs'; import * as smartfs from '@push.rocks/smartfs';
import * as smartinteract from '@push.rocks/smartinteract'; import * as smartinteract from '@push.rocks/smartinteract';
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
@@ -29,6 +30,7 @@ export {
npmextra, npmextra,
smartcli, smartcli,
smartdelay, smartdelay,
smartexit,
smartfs, smartfs,
smartinteract, smartinteract,
smartlog, smartlog,