Skip to content

Commit c8fe243

Browse files
authoredMar 23, 2025··
fix: improve detection and termination of orphan child processes (#519)
1 parent 26a06ed commit c8fe243

File tree

3 files changed

+63
-11
lines changed

3 files changed

+63
-11
lines changed
 

‎packages/cli/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shelve/cli",
3-
"version": "4.1.5",
3+
"version": "4.1.6",
44
"description": "The command-line interface for Shelve",
55
"homepage": "https://shelve.cloud",
66
"bugs": {
@@ -52,7 +52,8 @@
5252
"pkg-types": "1.3.1",
5353
"rc9": "^2.1.2",
5454
"semver": "7.7.1",
55-
"tinyglobby": "0.2.12"
55+
"tinyglobby": "0.2.12",
56+
"tree-kill": "^1.2.2"
5657
},
5758
"devDependencies": {
5859
"@types/bun": "1.2.5",

‎packages/cli/src/commands/run.ts

+51-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { defineCommand } from 'citty'
44
import { intro } from '@clack/prompts'
55
import type { EnvVar } from '@types'
66
import consola from 'consola'
7+
import treeKill from 'tree-kill'
78
import { handleCancel, loadShelveConfig } from '../utils'
89
import { EnvironmentService, EnvService, ProjectService } from '../services'
910
import { DEBUG } from '../constants'
@@ -51,6 +52,49 @@ export default defineCommand({
5152

5253
try {
5354
const isNpx = getNrBinPath() === 'npx'
55+
56+
let hasExited = false
57+
let exitTimeout: NodeJS.Timeout | null = null
58+
const childPid: number | null = null
59+
60+
const cleanupAndExit = (code: number = 0): void => {
61+
if (hasExited) return
62+
hasExited = true
63+
64+
if (exitTimeout) {
65+
clearTimeout(exitTimeout)
66+
}
67+
68+
process.exit(code)
69+
}
70+
71+
const handleSignal = (signal: string): void => {
72+
consola.info(`Received ${signal}, terminating process...`)
73+
74+
if (childPid) {
75+
treeKill(childPid, signal, (err) => {
76+
if (err && DEBUG) consola.error(`Failed to kill process: ${err}`)
77+
})
78+
79+
exitTimeout = setTimeout(() => {
80+
consola.warn('Process did not exit gracefully, forcing termination...')
81+
if (childPid) {
82+
treeKill(childPid, 'SIGKILL', () => {
83+
cleanupAndExit(1)
84+
})
85+
} else {
86+
cleanupAndExit(1)
87+
}
88+
}, 3000)
89+
} else {
90+
cleanupAndExit(0)
91+
}
92+
}
93+
94+
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
95+
process.on(signal, () => handleSignal(signal))
96+
})
97+
5498
const proc = x(
5599
getNrBinPath(),
56100
isNpx ? ['nr', command] : [command],
@@ -62,15 +106,13 @@ export default defineCommand({
62106
}
63107
)
64108

65-
const abortController = new AbortController()
66-
process.on('SIGINT', () => {
67-
consola.info('Exiting...')
68-
abortController.abort()
69-
proc.kill()
70-
process.exit(0)
71-
})
72-
73-
await proc
109+
try {
110+
await proc
111+
cleanupAndExit(0)
112+
} catch (err) {
113+
if (DEBUG) consola.error(err)
114+
cleanupAndExit(1)
115+
}
74116
} catch (error) {
75117
if (DEBUG) consola.error(error)
76118
process.exit(1)

‎pnpm-lock.yaml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.