diff --git a/packages/core/src/kilocode/pty/smoke.ts b/packages/core/src/kilocode/pty/smoke.ts index 2d2c447213..fa5a4e01be 100644 --- a/packages/core/src/kilocode/pty/smoke.ts +++ b/packages/core/src/kilocode/pty/smoke.ts @@ -17,7 +17,8 @@ export async function smoke() { const exited = Promise.withResolvers() const data = proc.onData((chunk) => { state.output += chunk - if (state.output.includes("KILO_PTY_READY")) output.resolve() + const lines = state.output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/) + if (lines.some((line) => line.trim() === "KILO_PTY_READY")) output.resolve() }) const exit = proc.onExit((event) => { state.exited = true diff --git a/packages/core/src/kilocode/pty/termination.ts b/packages/core/src/kilocode/pty/termination.ts index 51aa3d9f5d..c9862adbd8 100644 --- a/packages/core/src/kilocode/pty/termination.ts +++ b/packages/core/src/kilocode/pty/termination.ts @@ -1,4 +1,5 @@ import { spawn } from "child_process" +import { readdir, readFile } from "node:fs/promises" import { setTimeout as sleep } from "node:timers/promises" import type { Proc } from "../../pty/pty" import { Log } from "../../util/log" @@ -87,6 +88,15 @@ function signal(proc: Process, pids: number[], value: "SIGTERM" | "SIGKILL", inp } async function tree(file: string = "ps", args: string[] = ["-axo", "pid=,ppid="]) { + if (process.platform === "linux") { + try { + const rows = await procTree() + if (rows.length > 0) return rows + } catch (err) { + log.debug("failed to read Linux process tree", { err }) + } + } + return await new Promise>((resolve, reject) => { try { const child = spawn(file, args, { @@ -116,6 +126,21 @@ async function tree(file: string = "ps", args: string[] = ["-axo", "pid=,ppid="] }) } +async function procTree() { + const entries = await readdir("/proc", { withFileTypes: true }) + const rows = await Promise.all( + entries + .filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name)) + .map(async (entry) => { + const stat = await readFile(`/proc/${entry.name}/stat`, "utf8") + const match = stat.match(/^\d+ \(.*\) [A-Z] (\d+)/) + if (!match) return + return { pid: Number(entry.name), parent: Number(match[1]) } + }), + ) + return rows.filter((row): row is { pid: number; parent: number } => row !== undefined) +} + async function taskkill(file: string, args: string[], opts: { stdio: "ignore"; windowsHide: true; timeout: number }) { return await new Promise((resolve) => { try { diff --git a/packages/core/test/kilocode/pty-platform.test.ts b/packages/core/test/kilocode/pty-platform.test.ts index ed07f32de6..273eacdedc 100644 --- a/packages/core/test/kilocode/pty-platform.test.ts +++ b/packages/core/test/kilocode/pty-platform.test.ts @@ -21,6 +21,26 @@ const layer = AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ ]) const it = testEffect(layer) +async function alive(pid: number) { + if (process.platform !== "win32") { + try { + process.kill(pid, 0) + return true + } catch { + return false + } + } + + const proc = Bun.spawn(["tasklist", "/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { + stdout: "pipe", + stderr: "ignore", + windowsHide: true, + }) + const output = await new Response(proc.stdout).text() + await proc.exited + return output.includes(`"${pid}"`) +} + const attach = Effect.fn("PtyPlatformTest.attach")(function* (id: Pty.Info["id"]) { const pty = yield* Pty.Service const output = yield* Queue.unbounded() @@ -70,12 +90,7 @@ describe("cross-platform PTY", () => { expect(pid).toBeGreaterThan(0) yield* pty.remove(info.id) - if (process.platform === "win32") { - const result = yield* Effect.promise(() => Bun.$`taskkill /pid ${pid} /f /t`.quiet().nothrow()) - expect(result.exitCode).toBe(128) - } else { - expect(() => process.kill(pid, 0)).toThrow() - } + expect(yield* Effect.promise(() => alive(pid))).toBe(false) }), ) })