mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(wsl): reap the bash stdio relay's cats before it exits (#34260)
* fix(wsl): reap the bash stdio relay's cats before it exits The relay killed the surviving cat and exited at once. WSL's per-session init never reaps orphans, so that cat stayed a zombie, its session never ended, and the distro never stopped even with nothing of Orca's left in it (seen on every real-WSL idle-release run). Wait for both cats after the kill. * test(wsl): prove the bash relay waits for the cat it kills
This commit is contained in:
@@ -90,7 +90,8 @@ export function orcadBashStdioBridgeCommand(port: number): string {
|
||||
'cat <&3 & r=$!;',
|
||||
'cat <&4 >&3 & w=$!;',
|
||||
'exec 4<&-;',
|
||||
'wait -n; kill $r $w 2>/dev/null; exit 0; fi;',
|
||||
// Reap the cat just killed: WSL's session init never does, and its zombie keeps the distro up.
|
||||
'wait -n; kill $r $w 2>/dev/null; wait; exit 0; fi;',
|
||||
'eval "$1"'
|
||||
].join(' ')
|
||||
// Carry the Node bridge once, as data both shells can run.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createServer, type Server, type Socket } from 'node:net'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -64,6 +65,10 @@ function spawnBridge(port: number, env: Partial<NodeJS.ProcessEnv> = {}): Bridge
|
||||
})
|
||||
}
|
||||
|
||||
function realCat(): string {
|
||||
return spawnSync('sh', ['-c', 'command -v cat'], { encoding: 'utf8' }).stdout.trim()
|
||||
}
|
||||
|
||||
function exitOf(child: Bridge): Promise<number | null> {
|
||||
return new Promise((resolve) => child.once('exit', (code) => resolve(code)))
|
||||
}
|
||||
@@ -258,6 +263,41 @@ describe.runIf(RELAY)('the bash stdio bridge', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('waits for the cat it kills before exiting, so no zombie is left for the session init', async () => {
|
||||
// WSL's per-session init never reaps orphans; a zombie there kept an idle distro running (R40).
|
||||
// This cat takes a moment to die and leaves a mark, so the relay must wait to see it.
|
||||
const dir = mkdtempSync(join(scratch, 'reap-'))
|
||||
const bin = join(dir, 'bin')
|
||||
mkdirSync(bin)
|
||||
writeFileSync(
|
||||
join(bin, 'cat'),
|
||||
'#!/bin/sh\n' +
|
||||
`trap 'kill $c 2>/dev/null; sleep 0.3; : > "${join(dir, 'killed-cat')}"; exit 0' TERM\n` +
|
||||
`${realCat()} "$@" & c=$!\nwait $c\n` +
|
||||
// The relay starts the socket reader first; the stdin reader stays blocked as on a real
|
||||
// host, where the desktop keeps its pipe open.
|
||||
`mkdir "${join(dir, 'socket-reader')}" 2>/dev/null || { sleep 30 & c=$!; wait $c; }\n`,
|
||||
{ mode: 0o755 }
|
||||
)
|
||||
let accepted: Socket | null = null
|
||||
const server = createServer((socket) => {
|
||||
accepted = socket
|
||||
})
|
||||
const port = await listen(server)
|
||||
try {
|
||||
const child = spawnBridge(port, { PATH: `${bin}:${process.env.PATH}` })
|
||||
const exited = exitOf(child)
|
||||
await decode(child.stdout).ready
|
||||
await expect.poll(() => accepted).not.toBeNull()
|
||||
// orcad hangs up: the reading cat ends, the relay kills the one still reading stdin.
|
||||
accepted!.destroy()
|
||||
await expect(exited).resolves.toBe(0)
|
||||
expect(existsSync(join(dir, 'killed-cat'))).toBe(true)
|
||||
} finally {
|
||||
server.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('closes orcad’s socket as soon as the desktop ends stdin', async () => {
|
||||
const server = createServer()
|
||||
const ended = new Promise<void>((resolve) => {
|
||||
|
||||
Reference in New Issue
Block a user