From dcefc9e3d68c71be82e883e5ac502d0e3c9e39e3 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 6 Jun 2026 21:34:33 +0000 Subject: [PATCH] fix(debug): waited for dlv unix socket Used fs.stat to confirm delayed dlv Unix socket creation before connecting so Linux socket-mode adapters do not race Bun.connect. Added a delayed socket adapter regression test covering the launch path.\n\nFixes #2013 --- packages/coding-agent/src/dap/client.ts | 26 +++++----- .../test/debug/dap-launch-failures.test.ts | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/src/dap/client.ts b/packages/coding-agent/src/dap/client.ts index 79b333a82..54ea4581d 100644 --- a/packages/coding-agent/src/dap/client.ts +++ b/packages/coding-agent/src/dap/client.ts @@ -1,4 +1,5 @@ -import { logger, ptree } from "@oh-my-pi/pi-utils"; +import * as fs from "node:fs/promises"; +import { isEnoent, logger, ptree } from "@oh-my-pi/pi-utils"; import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env"; import { ToolAbortError } from "../tools/tool-errors"; import type { @@ -165,16 +166,8 @@ export class DapClient { detached: true, }); - // Wait for the socket file to appear (dlv needs to start listening) await waitForCondition( - () => { - try { - Bun.file(socketPath).size; - return true; - } catch { - return false; - } - }, + () => isUnixSocketReady(socketPath), 10_000, proc, ); @@ -553,15 +546,24 @@ export class DapClient { } } +async function isUnixSocketReady(socketPath: string): Promise { + try { + return (await fs.stat(socketPath)).isSocket(); + } catch (error) { + if (isEnoent(error)) return false; + throw error; + } +} + /** Poll a condition until it returns true, or timeout/process exit. */ async function waitForCondition( - check: () => boolean, + check: () => boolean | Promise, timeoutMs: number, proc: { exitCode: number | null }, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (check()) return; + if (await check()) return; if (proc.exitCode !== null) { throw new Error("Adapter process exited before socket was ready"); } diff --git a/packages/coding-agent/test/debug/dap-launch-failures.test.ts b/packages/coding-agent/test/debug/dap-launch-failures.test.ts index 83a133e95..004332d59 100644 --- a/packages/coding-agent/test/debug/dap-launch-failures.test.ts +++ b/packages/coding-agent/test/debug/dap-launch-failures.test.ts @@ -22,6 +22,32 @@ const TEST_ADAPTER: DapResolvedAdapter = { connectMode: "stdio", }; +const DELAYED_UNIX_SOCKET_ADAPTER = ` +const listenPrefix = "--listen=unix:"; +const listenArg = process.argv.find(arg => arg.startsWith(listenPrefix)); +if (!listenArg) { + throw new Error("missing --listen=unix argument"); +} +const socketPath = listenArg.slice(listenPrefix.length); +let server; +process.on("SIGTERM", () => { + server?.stop(); + process.exit(0); +}); +await Bun.sleep(100); +server = Bun.listen({ + unix: socketPath, + socket: { + open() {}, + data() {}, + close() {}, + error() {}, + }, +}); +await Bun.sleep(2_000); +server.stop(); +`; + type DapEventHandler = (body: unknown, event: DapEventMessage) => void | Promise; class FakeDapClient { @@ -286,6 +312,29 @@ describe("DAP launch failure handling", () => { expect(message).toContain("launch: 'C:\\repo\\program' is not a valid executable"); expect(message).toContain("configurationDone: Expected process to be stopped."); }); + + it("waits for delayed Unix socket adapters before connecting on Linux", async () => { + if (process.platform !== "linux") return; + const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "omp-debug-dlv-socket-")); + const adapterPath = path.join(cwd, "delayed-unix-socket-adapter.mjs"); + await fs.writeFile(adapterPath, DELAYED_UNIX_SOCKET_ADAPTER); + const adapter: DapResolvedAdapter = { + ...TEST_ADAPTER, + name: "dlv", + command: process.execPath, + args: [adapterPath], + resolvedCommand: process.execPath, + connectMode: "socket", + }; + let client: DapClient | undefined; + try { + client = await DapClient.spawn({ adapter, cwd }); + expect(client.isAlive()).toBe(true); + } finally { + await client?.dispose(); + await fs.rm(cwd, { recursive: true, force: true }); + } + }); }); describe("DebugTool launch validation", () => {