@take-out/scripts 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/helpers/handleProcessExit.ts +49 -140
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/run.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
|
-
"@take-out/helpers": "0.1.
|
|
32
|
+
"@take-out/helpers": "0.1.13",
|
|
33
33
|
"picocolors": "^1.1.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"vxrn": "^1.6.
|
|
36
|
+
"vxrn": "^1.6.13"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"vxrn": {
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"vxrn": "1.6.
|
|
44
|
+
"vxrn": "1.6.13"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
addProcessHandler,
|
|
5
3
|
setExitCleanupState,
|
|
@@ -7,8 +5,6 @@ import {
|
|
|
7
5
|
type ProcessType,
|
|
8
6
|
} from './run'
|
|
9
7
|
|
|
10
|
-
import type { ChildProcess } from 'node:child_process'
|
|
11
|
-
|
|
12
8
|
type ExitCallback = (info: { signal: NodeJS.Signals | string }) => void | Promise<void>
|
|
13
9
|
|
|
14
10
|
interface HandleProcessExitReturn {
|
|
@@ -17,96 +13,39 @@ interface HandleProcessExitReturn {
|
|
|
17
13
|
exit: (code?: number) => Promise<void>
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
return new Promise((resolve) => {
|
|
27
|
-
let output = ''
|
|
28
|
-
proc.stdout?.on('data', (data) => {
|
|
29
|
-
output += data.toString()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
proc.on('close', () => {
|
|
33
|
-
const childPids = output.trim().split('\n').filter(Boolean).map(Number)
|
|
34
|
-
resolve(childPids)
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error(`Error getting child PIDs for ${parentPid}: ${error}`)
|
|
39
|
-
return []
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function getAllDescendantPids(parentPid: number): Promise<number[]> {
|
|
44
|
-
const childPids = await getChildPids(parentPid)
|
|
45
|
-
const descendantPids = [...childPids]
|
|
46
|
-
|
|
47
|
-
for (const childPid of childPids) {
|
|
48
|
-
const grandchildren = await getAllDescendantPids(childPid)
|
|
49
|
-
descendantPids.push(...grandchildren)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return descendantPids
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function killProcessTree(
|
|
16
|
+
// kill an entire process group (works because children are spawned with detached: true,
|
|
17
|
+
// which makes them process group leaders). negative pid = kill the whole group.
|
|
18
|
+
// this is synchronous, no pgrep needed, no races.
|
|
19
|
+
function killProcessGroup(
|
|
56
20
|
pid: number,
|
|
57
21
|
signal: NodeJS.Signals = 'SIGTERM',
|
|
58
22
|
forceful: boolean = false
|
|
59
|
-
):
|
|
23
|
+
): void {
|
|
24
|
+
// kill the process group (negative pid)
|
|
60
25
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// process may already be gone
|
|
69
|
-
}
|
|
26
|
+
process.kill(-pid, signal)
|
|
27
|
+
} catch (_) {
|
|
28
|
+
// group may already be gone, try the individual process
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, signal)
|
|
31
|
+
} catch (_) {
|
|
32
|
+
// process already gone
|
|
70
33
|
}
|
|
34
|
+
}
|
|
71
35
|
|
|
72
|
-
|
|
36
|
+
if (forceful && signal !== 'SIGKILL') {
|
|
37
|
+
// schedule a SIGKILL followup
|
|
38
|
+
setTimeout(() => {
|
|
73
39
|
try {
|
|
74
|
-
process.kill(pid,
|
|
75
|
-
} catch (_) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// if forceful, wait briefly then send SIGKILL
|
|
81
|
-
if (forceful && signal !== 'SIGKILL') {
|
|
82
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
83
|
-
|
|
84
|
-
// send SIGKILL to any still-alive processes
|
|
85
|
-
for (const descendantPid of descendants.reverse()) {
|
|
86
|
-
try {
|
|
87
|
-
process.kill(descendantPid, 'SIGKILL')
|
|
88
|
-
} catch (_) {
|
|
89
|
-
// process may already be gone
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (pid && !Number.isNaN(pid)) {
|
|
94
|
-
try {
|
|
95
|
-
process.kill(pid, 'SIGKILL')
|
|
96
|
-
} catch (_) {
|
|
97
|
-
// process may already be gone
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch (error) {
|
|
102
|
-
console.error(`Error killing process tree for ${pid}: ${error}`)
|
|
40
|
+
process.kill(-pid, 'SIGKILL')
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 'SIGKILL')
|
|
44
|
+
} catch (_) {}
|
|
45
|
+
}, 100)
|
|
103
46
|
}
|
|
104
47
|
}
|
|
105
48
|
|
|
106
|
-
function isChildProcess(proc: ProcessType): proc is ChildProcess {
|
|
107
|
-
return 'killed' in proc && typeof (proc as any).on === 'function'
|
|
108
|
-
}
|
|
109
|
-
|
|
110
49
|
let isHandling = false
|
|
111
50
|
|
|
112
51
|
export function handleProcessExit({
|
|
@@ -120,26 +59,27 @@ export function handleProcessExit({
|
|
|
120
59
|
|
|
121
60
|
isHandling = true
|
|
122
61
|
const processes: ProcessType[] = []
|
|
123
|
-
|
|
124
|
-
let cleaning = false
|
|
62
|
+
let cleanupPromise: Promise<void> | null = null
|
|
125
63
|
|
|
126
|
-
const cleanup =
|
|
127
|
-
if
|
|
128
|
-
|
|
64
|
+
const cleanup = (signal: NodeJS.Signals | string = 'SIGTERM'): Promise<void> => {
|
|
65
|
+
// return existing cleanup promise if already running, so process.exit
|
|
66
|
+
// override waits for the real cleanup instead of exiting early
|
|
67
|
+
if (cleanupPromise) return cleanupPromise
|
|
129
68
|
|
|
130
|
-
|
|
69
|
+
cleanupPromise = doCleanup(signal)
|
|
70
|
+
return cleanupPromise
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const doCleanup = async (signal: NodeJS.Signals | string) => {
|
|
131
74
|
setExitCleanupState(true)
|
|
132
75
|
|
|
133
|
-
// suppress console output during cleanup for cleaner exit
|
|
134
76
|
if (signal === 'SIGINT') {
|
|
135
77
|
const noop = () => {}
|
|
136
78
|
console.log = noop
|
|
137
79
|
console.info = noop
|
|
138
80
|
console.warn = noop
|
|
139
|
-
// keep console.error for critical errors only
|
|
140
81
|
}
|
|
141
82
|
|
|
142
|
-
// wrap entire cleanup in a timeout to prevent hanging
|
|
143
83
|
if (onExit) {
|
|
144
84
|
try {
|
|
145
85
|
await onExit({ signal })
|
|
@@ -148,68 +88,35 @@ export function handleProcessExit({
|
|
|
148
88
|
}
|
|
149
89
|
}
|
|
150
90
|
|
|
151
|
-
// skip cleanup if no processes to clean up
|
|
152
91
|
if (processes.length === 0) {
|
|
153
92
|
return
|
|
154
93
|
}
|
|
155
94
|
|
|
156
|
-
//
|
|
95
|
+
// kill process groups synchronously - no pgrep, no races
|
|
96
|
+
// detached: true makes each child a process group leader,
|
|
97
|
+
// so kill(-pid) gets the entire group in one syscall
|
|
157
98
|
const isInterrupt = signal === 'SIGINT'
|
|
158
99
|
const killSignal = isInterrupt ? 'SIGTERM' : (signal as NodeJS.Signals)
|
|
159
100
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error(
|
|
167
|
-
`Error during graceful shutdown of process ${proc.pid}: ${error}`
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
)
|
|
101
|
+
for (const proc of processes) {
|
|
102
|
+
if (proc.pid) {
|
|
103
|
+
killProcessGroup(proc.pid, killSignal, isInterrupt)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
173
106
|
|
|
174
|
-
//
|
|
175
|
-
await new Promise((res) => setTimeout(res, isInterrupt ?
|
|
107
|
+
// brief wait for graceful shutdown
|
|
108
|
+
await new Promise((res) => setTimeout(res, isInterrupt ? 80 : 200))
|
|
176
109
|
|
|
177
|
-
// force kill any remaining
|
|
110
|
+
// force kill any remaining
|
|
178
111
|
for (const proc of processes) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
await killProcessTree(proc.pid, 'SIGKILL', true)
|
|
182
|
-
}
|
|
183
|
-
} catch (err) {
|
|
184
|
-
// process already gone
|
|
112
|
+
if (proc.pid && !proc.exitCode) {
|
|
113
|
+
killProcessGroup(proc.pid, 'SIGKILL')
|
|
185
114
|
}
|
|
186
115
|
}
|
|
187
116
|
}
|
|
188
117
|
|
|
189
118
|
const addChildProcess = (proc: ProcessType) => {
|
|
190
119
|
processes.push(proc)
|
|
191
|
-
|
|
192
|
-
// track pid if available
|
|
193
|
-
const pid = (proc as any).pid
|
|
194
|
-
if (pid) {
|
|
195
|
-
allChildPids.add(pid)
|
|
196
|
-
|
|
197
|
-
// for child processes, capture descendants once after a brief delay
|
|
198
|
-
if (isChildProcess(proc)) {
|
|
199
|
-
setTimeout(async () => {
|
|
200
|
-
if (proc.pid) {
|
|
201
|
-
try {
|
|
202
|
-
const childPids = await getChildPids(proc.pid)
|
|
203
|
-
for (const childPid of childPids) {
|
|
204
|
-
allChildPids.add(childPid)
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
// ignore errors in background polling
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}, 300)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
120
|
}
|
|
214
121
|
|
|
215
122
|
addProcessHandler(addChildProcess)
|
|
@@ -231,7 +138,9 @@ export function handleProcessExit({
|
|
|
231
138
|
})
|
|
232
139
|
}
|
|
233
140
|
|
|
234
|
-
// intercept process.exit to ensure cleanup
|
|
141
|
+
// intercept process.exit to ensure cleanup completes before exiting.
|
|
142
|
+
// if cleanup is already running, this awaits the SAME promise instead of
|
|
143
|
+
// early-returning and calling originalExit prematurely.
|
|
235
144
|
const originalExit = process.exit
|
|
236
145
|
process.exit = ((code?: number) => {
|
|
237
146
|
cleanup('SIGTERM').then(() => {
|