fix(dap): reject and bound the unix socket connect on Linux

connectSocket captured only resolve from Promise.withResolvers and its
error handler merely errored the stream, so a Bun.connect failure
(ECONNREFUSED/ENOENT/EACCES on a stat-ready-but-dead socket) never
settled the promise. The unbounded await in #spawnSocketUnix then hung
the launch forever, and the surrounding kill/reap guard could not fire.

Capture reject, track an opened flag, reject on error/close-before-open
and on the Bun.connect promise rejection, and bound the connect with a
timeout matching the launch deadline (cleared on settle). Thread
timeoutMs from #spawnSocketUnix. Export connectSocket for a deterministic
reject-path test.

Fixes #4087
This commit is contained in:
roboomp
2026-07-23 19:54:04 +00:00
parent fcf2274b2f
commit 3b194a3e37
2 changed files with 52 additions and 5 deletions
@@ -4,7 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import * as dapModule from "@oh-my-pi/pi-coding-agent/dap";
import { DapClient, waitForTcpServerListening } from "@oh-my-pi/pi-coding-agent/dap/client";
import { connectSocket, DapClient, waitForTcpServerListening } from "@oh-my-pi/pi-coding-agent/dap/client";
import { DapSessionManager } from "@oh-my-pi/pi-coding-agent/dap/session";
import type {
DapCapabilities,
@@ -487,6 +487,20 @@ describe("DAP launch failure handling", () => {
});
});
describe("connectSocket unix transport", () => {
it("rejects instead of hanging when the unix socket cannot be connected", async () => {
// A path that stat would report as a socket but that no one listens on
// yields ECONNREFUSED/ENOENT from Bun.connect. Before the fix the error
// handler only errored the stream and the returned promise never settled,
// so `await connectSocket(...)` hung the launch forever.
const deadSocket = path.join(os.tmpdir(), `omp-dap-dead-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`);
const start = Date.now();
await expect(connectSocket({ unix: deadSocket }, 5_000)).rejects.toThrow();
// Must settle on the connect error, not linger until the timeout bound.
expect(Date.now() - start).toBeLessThan(2_000);
});
});
describe("DAP TCP transport resilience", () => {
const TCP_ADAPTER_BASE: DapResolvedAdapter = {
...TEST_ADAPTER,