fix(subprocess): clean managed processes on host exit
This commit is contained in:
16
packages/subprocess/subprocess-local/tests/fixtures/managed-tree.ts
vendored
Normal file
16
packages/subprocess/subprocess-local/tests/fixtures/managed-tree.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
|
||||
const [statePath] = process.argv.slice(2)
|
||||
if (statePath === undefined) throw new Error('usage: managed-tree.ts <state-path>')
|
||||
|
||||
process.on('SIGTERM', () => {})
|
||||
process.on('SIGHUP', () => {})
|
||||
const descendant = spawn(process.execPath, [
|
||||
'-e',
|
||||
'process.on("SIGTERM",()=>{});process.on("SIGHUP",()=>{});setInterval(()=>{},60_000)',
|
||||
], { stdio: 'ignore' })
|
||||
if (descendant.pid === undefined) throw new Error('managed descendant did not publish a pid')
|
||||
|
||||
await writeFile(statePath, JSON.stringify({ root: process.pid, descendant: descendant.pid }))
|
||||
setInterval(() => {}, 60_000)
|
||||
79
packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts
vendored
Normal file
79
packages/subprocess/subprocess-local/tests/fixtures/process-exit-host.ts
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
import { access, readFile, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
const [kind, trigger, root] = process.argv.slice(2)
|
||||
if ((kind !== 'ordinary' && kind !== 'terminal')
|
||||
|| (trigger !== 'direct' && trigger !== 'uncaught-exception'
|
||||
&& trigger !== 'unhandled-rejection' && trigger !== 'dispose')
|
||||
|| root === undefined) {
|
||||
throw new Error('usage: process-exit-host.ts <ordinary|terminal> <direct|uncaught-exception|unhandled-rejection|dispose> <root>')
|
||||
}
|
||||
|
||||
const treeState = join(root, 'tree.json')
|
||||
const ready = join(root, 'ready')
|
||||
const proceed = join(root, 'proceed')
|
||||
const managedTree = fileURLToPath(new URL('./managed-tree.ts', import.meta.url))
|
||||
|
||||
async function waitForFile(path: string): Promise<void> {
|
||||
for (;;) {
|
||||
try {
|
||||
await access(path)
|
||||
return
|
||||
} catch (_notReady) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listenersBefore = process.listenerCount('exit')
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const listenersAfterLoad = process.listenerCount('exit')
|
||||
if (kind === 'ordinary') {
|
||||
ctx.subprocess.spawn({
|
||||
argv: [process.execPath, managedTree, treeState],
|
||||
cwd: process.cwd(),
|
||||
stdio: {
|
||||
stdin: 'ignore',
|
||||
stdout: { maxBytes: 1024 },
|
||||
stderr: { maxBytes: 1024 },
|
||||
},
|
||||
graceMs: trigger === 'dispose' ? 100 : 30_000,
|
||||
})
|
||||
} else {
|
||||
await ctx.subprocess.spawnTerminal({
|
||||
argv: [process.execPath, managedTree, treeState],
|
||||
cwd: process.cwd(),
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
graceMs: 30_000,
|
||||
})
|
||||
}
|
||||
|
||||
await waitForFile(treeState)
|
||||
const published = JSON.parse(await readFile(treeState, 'utf8')) as { root?: unknown; descendant?: unknown }
|
||||
if (!Number.isSafeInteger(published.root) || !Number.isSafeInteger(published.descendant)) {
|
||||
throw new Error('managed tree published invalid process ids')
|
||||
}
|
||||
await writeFile(ready, 'ready')
|
||||
await waitForFile(proceed)
|
||||
|
||||
if (trigger === 'dispose') {
|
||||
await fiber.dispose()
|
||||
await writeFile(join(root, 'dispose.json'), JSON.stringify({
|
||||
listenersBefore,
|
||||
listenersAfterLoad,
|
||||
listenersAfterDispose: process.listenerCount('exit'),
|
||||
}))
|
||||
} else if (trigger === 'direct') {
|
||||
process.exit(23)
|
||||
} else if (trigger === 'uncaught-exception') {
|
||||
setImmediate(() => { throw new Error('host-exit-uncaught-exception') })
|
||||
await new Promise(() => {})
|
||||
} else {
|
||||
void Promise.reject(new Error('host-exit-unhandled-rejection'))
|
||||
await new Promise(() => {})
|
||||
}
|
||||
Reference in New Issue
Block a user