@take-out/scripts 0.1.39-1772740363029 → 0.1.40
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 +6 -5
- package/src/build-initial.ts +1 -1
- package/src/dev-tunnel.ts +1 -1
- package/src/helpers/run.ts +14 -24
- package/src/release.ts +1 -1
- package/src/helpers/handleProcessExit.ts +0 -237
- package/src/helpers/process-cleanup.test.ts +0 -141
- package/src/helpers/resolveScript.test.ts +0 -187
- package/src/helpers/resolveScript.ts +0 -91
- package/src/process-cleanup.test.ts +0 -255
- package/src/run-pty.mjs +0 -413
- package/src/run.ts +0 -497
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./src/
|
|
5
|
+
"main": "./src/cmd.ts",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
-
"types": "./src/
|
|
10
|
-
"default": "./src/
|
|
9
|
+
"types": "./src/cmd.ts",
|
|
10
|
+
"default": "./src/cmd.ts"
|
|
11
11
|
},
|
|
12
12
|
"./package.json": "./package.json",
|
|
13
13
|
"./helpers/*": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
32
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
33
|
-
"@take-out/helpers": "0.1.
|
|
33
|
+
"@take-out/helpers": "0.1.40",
|
|
34
|
+
"@take-out/run": "0.1.40",
|
|
34
35
|
"picocolors": "^1.1.1"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
package/src/build-initial.ts
CHANGED
|
@@ -25,7 +25,7 @@ await cmd`bootstrap project workspace and build initial packages`.run(
|
|
|
25
25
|
await $`cd packages/helpers && bun run build`
|
|
26
26
|
|
|
27
27
|
// then build all other packages in parallel
|
|
28
|
-
await $`bun ./packages/
|
|
28
|
+
await $`bun ./packages/run/src/run.ts build --no-root`
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
package/src/dev-tunnel.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { cmd } from './cmd'
|
|
|
7
7
|
await cmd`set up cloudflare dev tunnel for local development`
|
|
8
8
|
.args('--port number')
|
|
9
9
|
.run(async ({ args, run, os, path }) => {
|
|
10
|
-
const { handleProcessExit } = await import('
|
|
10
|
+
const { handleProcessExit } = await import('@take-out/run/helpers/handleProcessExit')
|
|
11
11
|
|
|
12
12
|
handleProcessExit()
|
|
13
13
|
|
package/src/helpers/run.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
2
|
import { cpus } from 'node:os'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let isInExitCleanup = false
|
|
11
|
-
|
|
12
|
-
export function setExitCleanupState(state: boolean) {
|
|
13
|
-
isInExitCleanup = state
|
|
14
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
getIsExiting,
|
|
6
|
+
notifyProcessHandlers,
|
|
7
|
+
type ProcessHandler,
|
|
8
|
+
type ProcessType,
|
|
9
|
+
} from '@take-out/run/helpers/process-state'
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
return isInExitCleanup
|
|
18
|
-
}
|
|
11
|
+
import type { Timer } from '@take-out/helpers'
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
export type { ProcessHandler, ProcessType }
|
|
14
|
+
export { getIsExiting }
|
|
21
15
|
|
|
22
16
|
const colors = [
|
|
23
17
|
'\x1b[36m', // cyan
|
|
@@ -105,7 +99,7 @@ export async function run(
|
|
|
105
99
|
shell.unref()
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
|
|
102
|
+
notifyProcessHandlers(shell)
|
|
109
103
|
|
|
110
104
|
if (timeout) {
|
|
111
105
|
timeoutId = setTimeout(() => {
|
|
@@ -206,7 +200,7 @@ export async function run(
|
|
|
206
200
|
? `Command timed out after ${timeout}ms: ${command}`
|
|
207
201
|
: `Command failed with exit code ${exitCode}: ${command}`
|
|
208
202
|
|
|
209
|
-
if (!silent && !
|
|
203
|
+
if (!silent && !getIsExiting()) {
|
|
210
204
|
console.error(`run() error: ${errorMsg}: ${stderr || ''}`)
|
|
211
205
|
}
|
|
212
206
|
|
|
@@ -218,14 +212,14 @@ export async function run(
|
|
|
218
212
|
return { stdout, stderr, exitCode }
|
|
219
213
|
} catch (error) {
|
|
220
214
|
clearTimeout(timeoutId)
|
|
221
|
-
if (!silent && !
|
|
215
|
+
if (!silent && !getIsExiting()) {
|
|
222
216
|
// only show the error message, not the full object if it's our error
|
|
223
217
|
if (error instanceof Error && (error as any).cause?.exitCode !== undefined) {
|
|
224
218
|
// this is our controlled error, already logged above
|
|
225
219
|
} else {
|
|
226
220
|
console.error(`Error running command: ${command}`, error)
|
|
227
221
|
}
|
|
228
|
-
} else if (!silent &&
|
|
222
|
+
} else if (!silent && getIsExiting()) {
|
|
229
223
|
// simple message when being killed due to another error
|
|
230
224
|
const shortCmd = command.split(' ')[0]
|
|
231
225
|
console.error(`${shortCmd} exiting due to earlier error`)
|
|
@@ -235,10 +229,6 @@ export async function run(
|
|
|
235
229
|
}
|
|
236
230
|
}
|
|
237
231
|
|
|
238
|
-
export const addProcessHandler = (cb: ProcessHandler) => {
|
|
239
|
-
processHandlers.add(cb)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
232
|
export async function waitForRun(name: string) {
|
|
243
233
|
if (running[name] === undefined) {
|
|
244
234
|
throw new Error(`Can't wait before task runs: ${name}`)
|
package/src/release.ts
CHANGED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import { appendFileSync, rmSync } from 'node:fs'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
addProcessHandler,
|
|
6
|
-
setExitCleanupState,
|
|
7
|
-
type ProcessHandler,
|
|
8
|
-
type ProcessType,
|
|
9
|
-
} from './run'
|
|
10
|
-
|
|
11
|
-
type ExitCallback = (info: { signal: NodeJS.Signals | string }) => void | Promise<void>
|
|
12
|
-
|
|
13
|
-
interface HandleProcessExitReturn {
|
|
14
|
-
addChildProcess: ProcessHandler
|
|
15
|
-
cleanup: () => Promise<void>
|
|
16
|
-
exit: (code?: number) => Promise<void>
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const pidFilePath = `/tmp/tko-run-${process.pid}.pids`
|
|
20
|
-
|
|
21
|
-
function writePidFile(pid: number) {
|
|
22
|
-
try {
|
|
23
|
-
appendFileSync(pidFilePath, `${pid}\n`)
|
|
24
|
-
} catch {}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function removePidFile() {
|
|
28
|
-
try {
|
|
29
|
-
rmSync(pidFilePath, { force: true })
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// find descendant pids that survived the group kill
|
|
34
|
-
function findStragglers(trackedPids: number[]): number[] {
|
|
35
|
-
try {
|
|
36
|
-
const output = execSync('ps -eo pid,ppid', { encoding: 'utf-8', timeout: 2000 })
|
|
37
|
-
const lines = output.trim().split('\n').slice(1) // skip header
|
|
38
|
-
|
|
39
|
-
const parentToChildren = new Map<number, number[]>()
|
|
40
|
-
for (const line of lines) {
|
|
41
|
-
const parts = line.trim().split(/\s+/)
|
|
42
|
-
if (parts.length < 2) continue
|
|
43
|
-
const pid = parseInt(parts[0]!, 10)
|
|
44
|
-
const ppid = parseInt(parts[1]!, 10)
|
|
45
|
-
if (isNaN(pid) || isNaN(ppid)) continue
|
|
46
|
-
if (!parentToChildren.has(ppid)) parentToChildren.set(ppid, [])
|
|
47
|
-
parentToChildren.get(ppid)!.push(pid)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// walk from each tracked pid to find all descendants
|
|
51
|
-
const descendants = new Set<number>()
|
|
52
|
-
const queue = [...trackedPids]
|
|
53
|
-
while (queue.length > 0) {
|
|
54
|
-
const current = queue.pop()!
|
|
55
|
-
const children = parentToChildren.get(current)
|
|
56
|
-
if (!children) continue
|
|
57
|
-
for (const child of children) {
|
|
58
|
-
if (!descendants.has(child)) {
|
|
59
|
-
descendants.add(child)
|
|
60
|
-
queue.push(child)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return [...descendants]
|
|
65
|
-
} catch {
|
|
66
|
-
return []
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// kill entire process group to prevent orphaned descendants
|
|
71
|
-
function killProcess(
|
|
72
|
-
pid: number,
|
|
73
|
-
signal: NodeJS.Signals = 'SIGTERM',
|
|
74
|
-
forceful: boolean = false
|
|
75
|
-
): void {
|
|
76
|
-
// kill process group first (negative pid), then direct process
|
|
77
|
-
try {
|
|
78
|
-
process.kill(-pid, signal)
|
|
79
|
-
} catch (_) {}
|
|
80
|
-
try {
|
|
81
|
-
process.kill(pid, signal)
|
|
82
|
-
} catch (_) {}
|
|
83
|
-
|
|
84
|
-
if (forceful && signal !== 'SIGKILL') {
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
try {
|
|
87
|
-
process.kill(-pid, 'SIGKILL')
|
|
88
|
-
} catch (_) {}
|
|
89
|
-
try {
|
|
90
|
-
process.kill(pid, 'SIGKILL')
|
|
91
|
-
} catch (_) {}
|
|
92
|
-
}, 100)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
let isHandling = false
|
|
97
|
-
|
|
98
|
-
export function handleProcessExit({
|
|
99
|
-
onExit,
|
|
100
|
-
}: {
|
|
101
|
-
onExit?: ExitCallback
|
|
102
|
-
} = {}): HandleProcessExitReturn {
|
|
103
|
-
if (isHandling) {
|
|
104
|
-
throw new Error(`Only one handleProcessExit per process should be registered!`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
isHandling = true
|
|
108
|
-
const processes: ProcessType[] = []
|
|
109
|
-
let cleanupPromise: Promise<void> | null = null
|
|
110
|
-
|
|
111
|
-
const cleanup = (signal: NodeJS.Signals | string = 'SIGTERM'): Promise<void> => {
|
|
112
|
-
// return existing cleanup promise if already running, so process.exit
|
|
113
|
-
// override waits for the real cleanup instead of exiting early
|
|
114
|
-
if (cleanupPromise) return cleanupPromise
|
|
115
|
-
|
|
116
|
-
cleanupPromise = doCleanup(signal)
|
|
117
|
-
return cleanupPromise
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const doCleanup = async (signal: NodeJS.Signals | string) => {
|
|
121
|
-
setExitCleanupState(true)
|
|
122
|
-
|
|
123
|
-
if (onExit) {
|
|
124
|
-
try {
|
|
125
|
-
await onExit({ signal })
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error('Error in exit callback:', error)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (processes.length === 0) {
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const isInterrupt = signal === 'SIGINT'
|
|
136
|
-
const killSignal = isInterrupt ? 'SIGTERM' : (signal as NodeJS.Signals)
|
|
137
|
-
|
|
138
|
-
for (const proc of processes) {
|
|
139
|
-
if (proc.pid) {
|
|
140
|
-
killProcess(proc.pid, killSignal, isInterrupt)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// brief wait for graceful shutdown
|
|
145
|
-
await new Promise((res) => setTimeout(res, isInterrupt ? 80 : 200))
|
|
146
|
-
|
|
147
|
-
// force kill any remaining
|
|
148
|
-
for (const proc of processes) {
|
|
149
|
-
if (proc.pid && !proc.exitCode) {
|
|
150
|
-
killProcess(proc.pid, 'SIGKILL')
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// straggler walk: find any descendants that survived the group kill
|
|
155
|
-
const trackedPids = processes.map((p) => p.pid).filter(Boolean) as number[]
|
|
156
|
-
if (trackedPids.length > 0) {
|
|
157
|
-
const stragglers = findStragglers(trackedPids)
|
|
158
|
-
for (const pid of stragglers) {
|
|
159
|
-
try {
|
|
160
|
-
process.kill(pid, 'SIGKILL')
|
|
161
|
-
} catch {}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
removePidFile()
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const addChildProcess = (proc: ProcessType) => {
|
|
169
|
-
processes.push(proc)
|
|
170
|
-
if (proc.pid) writePidFile(proc.pid)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
addProcessHandler(addChildProcess)
|
|
174
|
-
|
|
175
|
-
let finalized = false
|
|
176
|
-
|
|
177
|
-
const finalizeWithSignal = (signal: NodeJS.Signals, fallbackCode: number) => {
|
|
178
|
-
if (finalized) return
|
|
179
|
-
finalized = true
|
|
180
|
-
process.off('SIGINT', sigintHandler)
|
|
181
|
-
process.off('SIGTERM', sigtermHandler)
|
|
182
|
-
process.off('SIGHUP', sighupHandler)
|
|
183
|
-
process.off('beforeExit', beforeExitHandler)
|
|
184
|
-
process.exit = originalExit
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
process.kill(process.pid, signal)
|
|
188
|
-
} catch {
|
|
189
|
-
originalExit(fallbackCode)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const sigtermHandler = () => {
|
|
194
|
-
cleanup('SIGTERM').then(() => {
|
|
195
|
-
finalizeWithSignal('SIGTERM', 143)
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const sigintHandler = () => {
|
|
200
|
-
cleanup('SIGINT').then(() => {
|
|
201
|
-
finalizeWithSignal('SIGINT', 130)
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const sighupHandler = () => {
|
|
206
|
-
cleanup('SIGHUP').then(() => {
|
|
207
|
-
finalizeWithSignal('SIGHUP', 129)
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// intercept process.exit to ensure cleanup completes before exiting.
|
|
212
|
-
// if cleanup is already running, this awaits the SAME promise instead of
|
|
213
|
-
// early-returning and calling originalExit prematurely.
|
|
214
|
-
const originalExit = process.exit
|
|
215
|
-
process.exit = ((code?: number) => {
|
|
216
|
-
cleanup('SIGTERM').then(() => {
|
|
217
|
-
originalExit(code)
|
|
218
|
-
})
|
|
219
|
-
}) as typeof process.exit
|
|
220
|
-
|
|
221
|
-
const beforeExitHandler = () => cleanup('SIGTERM')
|
|
222
|
-
process.on('beforeExit', beforeExitHandler)
|
|
223
|
-
process.on('SIGINT', sigintHandler)
|
|
224
|
-
process.on('SIGTERM', sigtermHandler)
|
|
225
|
-
process.on('SIGHUP', sighupHandler)
|
|
226
|
-
|
|
227
|
-
const exit = async (code: number = 0) => {
|
|
228
|
-
await cleanup('SIGTERM')
|
|
229
|
-
process.exit(code)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
addChildProcess,
|
|
234
|
-
cleanup,
|
|
235
|
-
exit,
|
|
236
|
-
}
|
|
237
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
import { afterAll, afterEach, describe, expect, it } from 'vitest'
|
|
7
|
-
|
|
8
|
-
function isAlive(pid: number): boolean {
|
|
9
|
-
try {
|
|
10
|
-
process.kill(pid, 0)
|
|
11
|
-
return true
|
|
12
|
-
} catch {
|
|
13
|
-
return false
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function waitForDead(pid: number, timeoutMs = 3000): Promise<boolean> {
|
|
18
|
-
const start = Date.now()
|
|
19
|
-
while (Date.now() - start < timeoutMs) {
|
|
20
|
-
if (!isAlive(pid)) return true
|
|
21
|
-
await new Promise((r) => setTimeout(r, 50))
|
|
22
|
-
}
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function waitForFile(path: string, timeoutMs = 3000): Promise<boolean> {
|
|
27
|
-
const start = Date.now()
|
|
28
|
-
while (Date.now() - start < timeoutMs) {
|
|
29
|
-
if (existsSync(path)) return true
|
|
30
|
-
await new Promise((r) => setTimeout(r, 50))
|
|
31
|
-
}
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('process cleanup', () => {
|
|
36
|
-
const tmpDir = mkdtempSync(join(tmpdir(), 'tko-test-'))
|
|
37
|
-
const pids: number[] = []
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
for (const pid of pids) {
|
|
41
|
-
try {
|
|
42
|
-
process.kill(-pid, 'SIGKILL')
|
|
43
|
-
} catch {}
|
|
44
|
-
try {
|
|
45
|
-
process.kill(pid, 'SIGKILL')
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
pids.length = 0
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
afterAll(() => {
|
|
52
|
-
try {
|
|
53
|
-
rmSync(tmpDir, { recursive: true, force: true })
|
|
54
|
-
} catch {}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('detached child gets its own process group', async () => {
|
|
58
|
-
const pidFile = join(tmpDir, 'child1.pid')
|
|
59
|
-
const child = spawn('bash', ['-c', `echo $$ > ${pidFile}; sleep 999`], {
|
|
60
|
-
detached: true,
|
|
61
|
-
stdio: 'ignore',
|
|
62
|
-
})
|
|
63
|
-
pids.push(child.pid!)
|
|
64
|
-
child.unref()
|
|
65
|
-
|
|
66
|
-
expect(await waitForFile(pidFile)).toBe(true)
|
|
67
|
-
const childPid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10)
|
|
68
|
-
|
|
69
|
-
expect(isAlive(childPid)).toBe(true)
|
|
70
|
-
|
|
71
|
-
// killing the process group should kill it
|
|
72
|
-
try {
|
|
73
|
-
process.kill(-child.pid!, 'SIGKILL')
|
|
74
|
-
} catch {}
|
|
75
|
-
expect(await waitForDead(childPid)).toBe(true)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('process group kill reaches grandchildren', async () => {
|
|
79
|
-
const grandchildPidFile = join(tmpDir, 'grandchild.pid')
|
|
80
|
-
// write a helper script so escaping is clean
|
|
81
|
-
const helperScript = join(tmpDir, 'grandchild-helper.sh')
|
|
82
|
-
writeFileSync(
|
|
83
|
-
helperScript,
|
|
84
|
-
`#!/bin/bash\necho $$ > ${grandchildPidFile}\nsleep 999\n`,
|
|
85
|
-
{ mode: 0o755 }
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
const parent = spawn('bash', ['-c', `bash ${helperScript} & wait`], {
|
|
89
|
-
detached: true,
|
|
90
|
-
stdio: 'ignore',
|
|
91
|
-
})
|
|
92
|
-
pids.push(parent.pid!)
|
|
93
|
-
|
|
94
|
-
expect(await waitForFile(grandchildPidFile)).toBe(true)
|
|
95
|
-
const grandchildPid = parseInt(readFileSync(grandchildPidFile, 'utf-8').trim(), 10)
|
|
96
|
-
pids.push(grandchildPid)
|
|
97
|
-
|
|
98
|
-
expect(isAlive(grandchildPid)).toBe(true)
|
|
99
|
-
|
|
100
|
-
// kill the process group — grandchild should die too
|
|
101
|
-
try {
|
|
102
|
-
process.kill(-parent.pid!, 'SIGKILL')
|
|
103
|
-
} catch {}
|
|
104
|
-
expect(await waitForDead(grandchildPid)).toBe(true)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('SIGTERM on parent with trap kills children', async () => {
|
|
108
|
-
const childPidFile = join(tmpDir, 'term-child.pid')
|
|
109
|
-
const wrapperScript = join(tmpDir, 'wrapper.sh')
|
|
110
|
-
writeFileSync(
|
|
111
|
-
wrapperScript,
|
|
112
|
-
[
|
|
113
|
-
'#!/bin/bash',
|
|
114
|
-
'cleanup() { kill 0; exit 0; }',
|
|
115
|
-
'trap cleanup SIGTERM',
|
|
116
|
-
`bash -c 'echo $$ > ${childPidFile}; sleep 999' &`,
|
|
117
|
-
'wait',
|
|
118
|
-
'',
|
|
119
|
-
].join('\n'),
|
|
120
|
-
{ mode: 0o755 }
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
const parent = spawn('bash', [wrapperScript], {
|
|
124
|
-
detached: true,
|
|
125
|
-
stdio: 'ignore',
|
|
126
|
-
})
|
|
127
|
-
pids.push(parent.pid!)
|
|
128
|
-
|
|
129
|
-
expect(await waitForFile(childPidFile)).toBe(true)
|
|
130
|
-
const childPid = parseInt(readFileSync(childPidFile, 'utf-8').trim(), 10)
|
|
131
|
-
pids.push(childPid)
|
|
132
|
-
|
|
133
|
-
expect(isAlive(childPid)).toBe(true)
|
|
134
|
-
|
|
135
|
-
// send SIGTERM to the process group
|
|
136
|
-
try {
|
|
137
|
-
process.kill(-parent.pid!, 'SIGTERM')
|
|
138
|
-
} catch {}
|
|
139
|
-
expect(await waitForDead(childPid, 5000)).toBe(true)
|
|
140
|
-
})
|
|
141
|
-
})
|