fix(dap): removed per-write exit handlers

This commit is contained in:
can1357
2026-07-02 23:51:18 +02:00
parent ab75b1b7d1
commit d0fa896c19
+22 -6
View File
@@ -71,6 +71,8 @@ export class DapClient {
#eventHandlers = new Map<string, Set<DapEventHandler>>();
#anyEventHandlers = new Set<DapEventHandler>();
#reverseRequestHandlers = new Map<string, DapReverseRequestHandler>();
#adapterExited = false;
#pendingWriteExitRejectors = new Set<() => void>();
constructor(
adapter: DapResolvedAdapter,
@@ -84,6 +86,10 @@ export class DapClient {
this.#readable = options?.readable ?? (proc.stdout as ReadableStream<Uint8Array>);
this.#writeSink = options?.writeSink ?? proc.stdin;
this.#socket = options?.socket;
this.proc.exited.then(
() => this.#rejectPendingWritesForExit(),
() => this.#rejectPendingWritesForExit(),
);
}
static async spawn({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise<DapClient> {
@@ -417,6 +423,10 @@ export class DapClient {
const flushResult = this.#writeSink.flush();
if (!(flushResult instanceof Promise)) return;
if (this.#adapterExited) {
throw new Error(`DAP adapter ${this.adapter.name} exited before write completed`);
}
const { promise: guardPromise, reject: guardReject, resolve: guardResolve } = Promise.withResolvers<void>();
const timer = setTimeout(
() =>
@@ -425,13 +435,10 @@ export class DapClient {
),
WRITE_MESSAGE_TIMEOUT_MS,
);
// If the adapter exits mid-write, fail fast rather than blocking forever
// on a stdin that will never drain. `proc.exited` may resolve normally
// (clean exit) or reject (non-zero); either way the write is doomed.
const onExit = () => {
const rejectOnExit = () => {
guardReject(new Error(`DAP adapter ${this.adapter.name} exited before write completed`));
};
this.proc.exited.then(onExit, onExit);
this.#pendingWriteExitRejectors.add(rejectOnExit);
try {
await Promise.race([flushResult, guardPromise]);
@@ -442,11 +449,20 @@ export class DapClient {
throw error;
} finally {
clearTimeout(timer);
// Release the guard so any late onExit call becomes a no-op.
this.#pendingWriteExitRejectors.delete(rejectOnExit);
// Release the guard so any late timeout callback becomes a no-op.
guardResolve();
}
}
#rejectPendingWritesForExit(): void {
this.#adapterExited = true;
for (const reject of this.#pendingWriteExitRejectors) {
reject();
}
this.#pendingWriteExitRejectors.clear();
}
async dispose(): Promise<void> {
if (this.#disposed) return;
this.#disposed = true;