Файли
openidea/src/features/run/applicationUpdate.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

153 рядки
6.6 KiB
TypeScript

import { tx } from "@/platform/i18n";
import { create } from "zustand";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { executeAction, getAction } from "@/platform/actions";
import { call, commands, isTauri, nextRequestId, type RunConfig } from "@/platform/ipc";
import { parseJavaArgs, resolveJavaModule } from "@/platform/javaLaunch";
import {
maybeEditorService,
notifications,
useDebugRunSessions,
type DebugServiceSession,
} from "@/workbench/contributions";
import { showContextMenu } from "@/ui";
import { buildTomcatArtifact, syncTomcatOutputs } from "./tomcat";
import { runUpdateBuild } from "./buildUpdate";
import { rerunConfig, startRun, stopSession, useRun, waitForExit, type RunSession } from "./store";
import { isUpdatable, updateAllowed, type UpdateMode } from "./updatePolicies";
export type UpdateSession = Pick<RunSession, "id" | "config"> & { debug?: DebugServiceSession };
export const useApplicationUpdates = create<{ ids: ReadonlySet<string> }>(() => ({ ids: new Set() }));
const updatingConfigs = new Set<string>();
export function updatableSessions(): UpdateSession[] {
const runs = useRun.getState().sessions.filter((s) => isUpdatable(s.config) && s.status === "running");
const debug = useDebugRunSessions
.getState()
.serviceSessions.filter(
(s) =>
isUpdatable(s.config) &&
(s.status === "running" || s.status === "paused") &&
!runs.some((r) => r.runId === s.processId && r.config.id === s.config.id),
)
.map((s) => ({ id: `debug:${s.id}`, config: s.config, debug: s }));
return [...runs, ...debug];
}
function params(session: UpdateSession): RunConfig["params"] {
return useRun.getState().configs.find((c) => c.id === session.config.id)?.params ?? session.config.params;
}
async function updateSpringBoot(config: RunConfig, mode: UpdateMode, debugging: boolean) {
const module = await resolveJavaModule(config);
const parsed = parseJavaArgs(config.args);
if (!parsed.mainClass) throw new Error(tx("The Spring Boot configuration has no main class"));
const info = await call(
commands.javaResolveLaunch(module, parsed.classpath === "test", mode === "classes", nextRequestId()),
);
if (info.build === "failed" || info.build === "withErrors" || info.build === "cancelled")
throw new Error(tx("Application update build failed"));
await runUpdateBuild(await call(commands.runApplicationBuild(config, module)));
const classPaths = parsed.explicitClasspath?.split(info.pathListSeparator) ?? info.classPaths;
await call(commands.runApplicationPublish(config, classPaths, parsed.mainClass));
if (mode === "classes" && debugging && (await executeAction("debug.reloadChangedClasses", { configId: config.id })))
return;
notifications().notify({
group: "run",
level: "info",
title: "Resources updated",
...(mode === "classes"
? {
body: "Classes were compiled. Restart the application to load them, or launch with the debugger to use HotSwap.",
}
: {}),
});
}
export async function updateApplication(session: UpdateSession, mode: string): Promise<void> {
if (!updateAllowed(session.config, mode) || updatingConfigs.has(session.config.id)) return;
updatingConfigs.add(session.config.id);
useApplicationUpdates.setState((s) => ({ ids: new Set([...s.ids, session.id]) }));
try {
const debugging =
!!session.debug || useDebugRunSessions.getState().sessions.some((s) => s.configId === session.config.id);
if (mode === "restart") {
const config = rerunConfig(session);
if (session.debug) {
await session.debug.stop();
await executeAction("run.debugConfig", config);
} else if (debugging) {
await stopSession(session.id);
await waitForExit([session.id], null);
await executeAction("run.debugConfig", config);
} else await startRun(config, session.id);
return;
}
await maybeEditorService()?.saveAll();
if (session.config.kind === "spring-boot") {
await updateSpringBoot(session.config, mode, debugging);
} else {
if (mode === "redeploy" || !(await syncTomcatOutputs(session.config, mode === "resources"))) {
await buildTomcatArtifact(session.config);
await call(commands.runTomcatUpdate(session.config, mode));
}
if (mode === "classes" && debugging) {
const swapped =
getAction("debug.reloadChangedClasses") &&
(await executeAction("debug.reloadChangedClasses", { configId: session.config.id }));
if (!swapped) await call(commands.runTomcatUpdate(session.config, "redeploy"));
}
notifications().notify({ group: "run", level: "info", title: "Application updated" });
}
if (!session.debug) useRun.setState({ activeSession: session.id });
} catch (e) {
notifications().notify({ group: "run", level: "error", title: "Application update failed", body: String(e) });
} finally {
updatingConfigs.delete(session.config.id);
useApplicationUpdates.setState((s) => ({ ids: new Set([...s.ids].filter((id) => id !== session.id)) }));
}
}
export function showApplicationUpdate(sessionId?: string): void {
const state = useRun.getState();
const candidates = updatableSessions();
const session = candidates.find((s) => s.id === (sessionId || state.activeSession)) ?? candidates[0];
if (!session) return;
const policy = params(session)?.onUpdate ?? "ask";
if (policy !== "ask" && updateAllowed(session.config, policy)) {
void updateApplication(session, policy);
return;
}
const choices = [
["resources", "Update resources"],
["classes", "Update classes and resources"],
...(session.config.kind === "tomcat" ? [["redeploy", "Redeploy"]] : []),
["restart", session.config.kind === "tomcat" ? "Restart server" : "Restart"],
];
showContextMenu({ x: window.innerWidth / 2 - 140, y: 100 }, [
{ kind: "header", label: "Update Application" },
...choices.map(([mode, label]) => ({
label: label!,
disabled: useApplicationUpdates.getState().ids.has(session.id) || !updateAllowed(session.config, mode!),
run: () => {
void updateApplication(session, mode!);
},
})),
]);
}
export function updateOnDeactivation(): void {
for (const session of updatableSessions()) {
const mode = params(session)?.onDeactivation;
if (mode === "resources" || mode === "classes") void updateApplication(session, mode);
}
}
export function listenForApplicationDeactivation(): () => void {
if (!isTauri()) return () => {};
const listener = getCurrentWebviewWindow().listen("ide://application-deactivated", updateOnDeactivation);
return () => {
void listener.then((unlisten) => unlisten());
};
}