Файли
openidea/src/features/run/stopStatus.test.ts
T
misswellтаmisswell 438ab158ad fix(run): keep Stop in effect until the process is really gone (#1763)
Stop flipped the session back to Run as soon as the direct child exited, but a shell, Maven or the debug adapter can exit while the application it started still holds its port, so the next Run failed with "port already in use". The console also went quiet on a debug Stop because the DAP session was disconnected before java-debug drained the program's output, and closing a window killed running programs with SIGKILL at once.

Run and Debug sessions now stay in a stopping state from the Stop press until the whole process group (or the debuggee's pid tree) is gone. As in IntelliJ IDEA, the first Stop only interrupts and waits, and a second Stop kills; the Stop button turns into Kill process meanwhile. A rerun or restart waits for the old process without a time limit, and the termination time limit setting now only bounds a configuration's stop command. Debug launches record the program pid from the adapter's process/processid event, signal its pid tree and keep the adapter attached until the program exits; closing a project also stops debug sessions under its root. Closing a window with running programs shows a Terminating panel with Kill process and closes once they are gone.

Co-authored-by: misswell <[email protected]>
2026-11-07 10:15:30 +08:00

75 рядки
2.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RunConfig } from "@/platform/ipc";
const runStop = vi.hoisted(() => vi.fn(async () => ({ status: "ok" as const, data: null })));
vi.mock("@/platform/ipc", async (importOriginal) => {
const real = await importOriginal<typeof import("@/platform/ipc")>();
return {
...real,
isTauri: () => true,
commands: { ...real.commands, runStop },
call: async (p: Promise<{ status: "ok"; data: unknown }>) => (await p).data,
};
});
vi.mock("@/workbench/contributions", async (importOriginal) => {
const real = await importOriginal<typeof import("@/workbench/contributions")>();
return { ...real, notifications: () => ({ notify: () => undefined }) };
});
import { isLive, onRunExited, stopSession, useRun, type RunSession } from "./store";
const config: RunConfig = {
id: "app",
name: "app",
kind: "spring-boot",
command: "java",
args: [],
cwd: null,
env: {},
detected: false,
isTest: false,
debugAdapter: null,
};
const session: RunSession = {
id: "s1",
config,
runId: 7 as RunSession["runId"],
status: "running",
exitCode: null,
startedAt: 0,
endedAt: null,
stdinClosed: false,
};
beforeEach(() => {
runStop.mockClear();
useRun.setState({ sessions: [session], activeSession: "s1" });
});
describe("Stop keeps the tab live until the process is gone", () => {
it("a stopping run counts as live", () => {
for (const status of ["starting", "running", "stopping"] as const) expect(isLive({ status }), status).toBe(true);
for (const status of ["finished", "failed"] as const) expect(isLive({ status }), status).toBe(false);
});
it("stays stopping after the stop request and only finishes on the exit event", async () => {
await stopSession("s1");
expect(runStop).toHaveBeenCalledWith(session.runId);
expect(useRun.getState().sessions[0]).toMatchObject({ status: "stopping", stopped: true });
// The button stays Stop while the process shuts down: Run must not start again yet.
expect(isLive(useRun.getState().sessions[0]!)).toBe(true);
onRunExited(session.runId!, 130);
await new Promise((r) => setTimeout(r, 10));
expect(useRun.getState().sessions[0]?.status).toBe("finished");
expect(isLive(useRun.getState().sessions[0]!)).toBe(false);
});
it("a second Stop while stopping asks the server to kill it", async () => {
await stopSession("s1");
await stopSession("s1");
expect(runStop).toHaveBeenCalledTimes(2);
});
});