mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(debugger): retain verified detach across cleanup retries
This commit is contained in:
@@ -174,7 +174,11 @@ const finishExitedDebuggerSession = async (
|
||||
return { status };
|
||||
};
|
||||
|
||||
/** Stop only the directly owned debugger process, never its process group or target. */
|
||||
/**
|
||||
* Stop only the directly owned debugger process, never its process group or target.
|
||||
* The caller must permanently close command admission before the first attempt;
|
||||
* retries retain completed detach facts while process termination is pending.
|
||||
*/
|
||||
export const stopOwnedDebuggerProcess = async (
|
||||
session: OwnedDebuggerProcess,
|
||||
debuggerName: string,
|
||||
@@ -218,36 +222,42 @@ export const stopOwnedDebuggerProcess = async (
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} process cannot be stopped safely on Windows because job termination can also terminate its inferior processes.`,
|
||||
};
|
||||
try {
|
||||
if (session.detachInferior === undefined) {
|
||||
detachOutcomes.set(session, "unconfirmed");
|
||||
// Admission remains closed once cleanup starts. A verified detach is an
|
||||
// owned completion fact, even when terminating the debugger later fails.
|
||||
// Retrying the command transport can no longer improve that fact and may
|
||||
// block termination if shutdown already made the transport unavailable.
|
||||
if (detachOutcomes.get(session) !== "confirmed") {
|
||||
try {
|
||||
if (session.detachInferior === undefined) {
|
||||
detachOutcomes.set(session, "unconfirmed");
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} inferior detach cannot be verified because the provider has no detach capability.`,
|
||||
};
|
||||
}
|
||||
const detached = await waitForInferiorDetach(
|
||||
session,
|
||||
session.detachInferior,
|
||||
);
|
||||
if (detached === undefined)
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} inferior detach was not verified within ${String(DETACH_TIMEOUT_MS)} ms. This release has no runtime override.`,
|
||||
};
|
||||
if (!isVerifiedDetachResult(detached)) {
|
||||
detachOutcomes.set(session, "unconfirmed");
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} provider did not confirm no inferior or detachment with a durable output sink.`,
|
||||
};
|
||||
}
|
||||
detachOutcomes.set(session, "confirmed");
|
||||
} catch (cause: unknown) {
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} inferior detach cannot be verified because the provider has no detach capability.`,
|
||||
reason: `Owned ${debuggerName} inferior detach could not be verified: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
};
|
||||
}
|
||||
const detached = await waitForInferiorDetach(
|
||||
session,
|
||||
session.detachInferior,
|
||||
);
|
||||
if (detached === undefined)
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} inferior detach was not verified within ${String(DETACH_TIMEOUT_MS)} ms. This release has no runtime override.`,
|
||||
};
|
||||
if (!isVerifiedDetachResult(detached)) {
|
||||
detachOutcomes.set(session, "unconfirmed");
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} provider did not confirm no inferior or detachment with a durable output sink.`,
|
||||
};
|
||||
}
|
||||
detachOutcomes.set(session, "confirmed");
|
||||
} catch (cause: unknown) {
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} inferior detach could not be verified: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
};
|
||||
}
|
||||
session.launched.process.kill("SIGTERM");
|
||||
if (!(await session.supervisor.waitForExit(300))) {
|
||||
|
||||
@@ -224,6 +224,17 @@ it("retains process supervision after an incomplete stop and disposes after a re
|
||||
let disposals = 0;
|
||||
let outputUnrefs = 0;
|
||||
const signals: NodeJS.Signals[] = [];
|
||||
const detachInferior = vi
|
||||
.fn<
|
||||
() => Promise<{
|
||||
readonly status: "detached";
|
||||
readonly outputSink: "durable";
|
||||
}>
|
||||
>()
|
||||
.mockResolvedValueOnce({ status: "detached", outputSink: "durable" })
|
||||
.mockRejectedValue(
|
||||
new Error("debugger transport closed during termination"),
|
||||
);
|
||||
const session = {
|
||||
id: "debugger-session",
|
||||
exited: false,
|
||||
@@ -235,11 +246,7 @@ it("retains process supervision after an incomplete stop and disposes after a re
|
||||
},
|
||||
},
|
||||
},
|
||||
detachInferior: async () =>
|
||||
({
|
||||
status: "detached",
|
||||
outputSink: "durable",
|
||||
}) as const,
|
||||
detachInferior,
|
||||
supervisor: {
|
||||
waitForExit: async (): Promise<boolean> => {
|
||||
waits += 1;
|
||||
@@ -273,6 +280,7 @@ it("retains process supervision after an incomplete stop and disposes after a re
|
||||
expect(retry).toEqual({ status: "stopped" });
|
||||
expect(disposals).toBe(1);
|
||||
expect(waits).toBe(3);
|
||||
expect(detachInferior).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("refuses Windows job-backed debugger termination before signaling", async () => {
|
||||
@@ -694,11 +702,6 @@ it("retains an owned debugger when inferior detach does not settle", async () =>
|
||||
},
|
||||
detachInferior: () => {
|
||||
detachCalls += 1;
|
||||
if (detachCalls > 1)
|
||||
return Promise.resolve({
|
||||
status: "detached",
|
||||
outputSink: "durable",
|
||||
} as const);
|
||||
return new Promise<{
|
||||
readonly status: "detached";
|
||||
readonly outputSink: "durable";
|
||||
@@ -744,7 +747,7 @@ it("retains an owned debugger when inferior detach does not settle", async () =>
|
||||
expect(
|
||||
await stopOwnedDebuggerProcess(session, "GDB", { platform: "linux" }),
|
||||
).toEqual({ status: "stopped" });
|
||||
expect(detachCalls).toBe(2);
|
||||
expect(detachCalls).toBe(1);
|
||||
expect(signals).toEqual(["SIGTERM"]);
|
||||
expect(disposals).toBe(1);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user