дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(tasks): Make every background task cancellable, queued or running (#177)
Background tasks often couldn't be cancelled, or cancelling didn't stop them: - A cancel that arrived before the backend registered the request was dropped (build while saving files, multi-repository fetch/pull between repositories). The host now remembers early cancels (bounded, 2 min), and request ids start at a random value per page. - There was no queue and no common cancel path: backend-reported tasks (LSP progress, deployment transfers) couldn't be cancelled, and tool installs, VCS operations and update checks had no cancel button in Background Tasks. - Only the direct child process was killed, and JDK / tool archives were extracted without checking for cancellation. Tool installs and JDK downloads now share a downloads queue; cancelling a queued task removes it without starting any work. Every task cancels through one path that shows "Cancelling…" and then removes it; tasks that can't be interrupted show a disabled cancel button with the reason. IdeEvent::Task carries `cancellable` with a cancel_task command, cancellable LSP progress sends window/workDoneProgress/cancel, installers, curl and tar run in their own process group and are killed as a group, and extraction checks for cancellation between entries. Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
Цей коміт міститься в:
1 джерело
c6621c5c36
коміт
1c46a1cd04
40 files changed
+888
-148
No files matched your search
@@ -70,6 +70,22 @@ describe("tools store", () => {
|
||||
expect(useTools.getState().tools.find((t) => t.name === "gopls")?.status).toBe("missing");
|
||||
});
|
||||
|
||||
it("installs one at a time; one cancelled while it waits never starts", async () => {
|
||||
backend.stepMs = 10;
|
||||
await refreshTools();
|
||||
const install = vi.spyOn(backend, "install");
|
||||
const first = installTool("gopls");
|
||||
const queued = installTool("vtsls");
|
||||
expect(useTasks.getState().tasks.find((t) => t.id === "tools:vtsls")).toMatchObject({ queued: true });
|
||||
cancelInstall("vtsls");
|
||||
expect(useTasks.getState().tasks.find((t) => t.id === "tools:vtsls")).toBeUndefined();
|
||||
expect(await queued).toBeNull();
|
||||
expect(useTools.getState().logs["vtsls"]?.error).toBe("Cancelled");
|
||||
expect((await first)?.status).toBe("installed");
|
||||
expect(install.mock.calls.map((c) => c[0])).toEqual(["gopls"]);
|
||||
expect(notes.filter((n) => n.level === "error")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("uninstalls", async () => {
|
||||
await refreshTools();
|
||||
expect(await uninstallTool("rust-analyzer")).toBe(true);
|
||||
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
requiredJdk,
|
||||
TEMURIN,
|
||||
type DownloadJdkArgs,
|
||||
type JdkDownload,
|
||||
} from "@/platform/jdk";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { upsertTask } from "@/workbench/tasks";
|
||||
import { queueTask, upsertTask } from "@/workbench/tasks";
|
||||
import { DOWNLOAD_QUEUE } from "./store";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
const JAVA_SETTINGS = "languages.java";
|
||||
@@ -42,16 +44,19 @@ export function downloadJdkFor(args: DownloadJdkArgs): Promise<Sdk | null> {
|
||||
const title = `Downloading JDK ${args.version}`;
|
||||
const taskId = `jdk-download:${args.version}`;
|
||||
const key = `jdk-download-${args.version}`;
|
||||
const cancel = () => job.cancel();
|
||||
const job = downloadJdk(TEMURIN, args.version, null, (e) => {
|
||||
const message = e.type === "line" ? e.text : e.message;
|
||||
const fraction = e.type === "progress" ? e.fraction : null;
|
||||
upsertTask({ id: taskId, title, message, fraction, done: false, cancel });
|
||||
});
|
||||
upsertTask({ id: taskId, title, message: "Eclipse Temurin", fraction: null, done: false, cancel });
|
||||
const p = (async () => {
|
||||
try {
|
||||
const sdk = await job.promise;
|
||||
// Waits for the downloads ahead of it; cancelling it there means it never starts.
|
||||
const sdk = await queueTask(DOWNLOAD_QUEUE, { id: taskId, title }, () => {
|
||||
const cancel = () => job.cancel();
|
||||
upsertTask({ id: taskId, title, message: "Eclipse Temurin", fraction: null, done: false, cancel });
|
||||
const job: JdkDownload = downloadJdk(TEMURIN, args.version, null, (e) => {
|
||||
const message = e.type === "line" ? e.text : e.message;
|
||||
const fraction = e.type === "progress" ? e.fraction : null;
|
||||
upsertTask({ id: taskId, title, message, fraction, done: false, cancel });
|
||||
});
|
||||
return job.promise;
|
||||
});
|
||||
if (args.languageServer) await chooseForLanguageServer(sdk.path);
|
||||
notifications().notify({
|
||||
group: "tools",
|
||||
|
||||
@@ -4,11 +4,13 @@ import type { RequestId, ToolInfo } from "@/bindings";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { isCancelled, nextRequestId } from "@/platform/ipc";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { upsertTask } from "@/workbench/tasks";
|
||||
import { cancelTask, queueTask, upsertTask, useTasks } from "@/workbench/tasks";
|
||||
import { toolsBackend } from "./backend";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
export const SETTINGS_PAGE_ID = "languages.servers";
|
||||
/** Tool installs and JDK downloads take turns (one network download / package manager at a time). */
|
||||
export const DOWNLOAD_QUEUE = "downloads";
|
||||
const MAX_LOG_LINES = 400;
|
||||
|
||||
export interface InstallJob {
|
||||
@@ -100,24 +102,25 @@ export function installTool(name: string, opts: { notificationKey?: string } = {
|
||||
const taskId = `tools:${name}`;
|
||||
const key = opts.notificationKey ?? `tools-install-${name}`;
|
||||
useTools.setState((s) => ({ jobs: { ...s.jobs, [name]: { request, lines: [], fraction: null, message: null } } }));
|
||||
upsertTask({ id: taskId, title: tx`Installing ${display}`, message: null, fraction: null, done: false });
|
||||
const title = tx`Installing ${display}`;
|
||||
const cancel = () => toolsBackend().cancel(request);
|
||||
const task = (message: string | null, fraction: number | null) =>
|
||||
upsertTask({ id: taskId, title, message, fraction, done: false, cancel });
|
||||
|
||||
const p = (async () => {
|
||||
try {
|
||||
const info = await toolsBackend().install(name, request, (e) => {
|
||||
if (e.type === "line") {
|
||||
patchJob(name, (j) => ({ ...j, lines: [...j.lines, e.text].slice(-MAX_LOG_LINES), message: e.text }));
|
||||
upsertTask({ id: taskId, title: tx`Installing ${display}`, message: e.text, fraction: null, done: false });
|
||||
} else {
|
||||
patchJob(name, (j) => ({ ...j, fraction: e.fraction, message: e.message }));
|
||||
upsertTask({
|
||||
id: taskId,
|
||||
title: tx`Installing ${display}`,
|
||||
message: e.message,
|
||||
fraction: e.fraction,
|
||||
done: false,
|
||||
});
|
||||
}
|
||||
// Downloads and package managers run one at a time; the rest wait, cancellable.
|
||||
const info = await queueTask(DOWNLOAD_QUEUE, { id: taskId, title }, () => {
|
||||
task(null, null);
|
||||
return toolsBackend().install(name, request, (e) => {
|
||||
if (e.type === "line") {
|
||||
patchJob(name, (j) => ({ ...j, lines: [...j.lines, e.text].slice(-MAX_LOG_LINES), message: e.text }));
|
||||
task(e.text, null);
|
||||
} else {
|
||||
patchJob(name, (j) => ({ ...j, fraction: e.fraction, message: e.message }));
|
||||
task(e.message, e.fraction);
|
||||
}
|
||||
});
|
||||
});
|
||||
upsertTool(info);
|
||||
endJob(name, null);
|
||||
@@ -151,7 +154,7 @@ export function installTool(name: string, opts: { notificationKey?: string } = {
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
upsertTask({ id: taskId, title: tx`Installing ${display}`, message: null, fraction: 1, done: true });
|
||||
upsertTask({ id: taskId, title, message: null, fraction: 1, done: true });
|
||||
running.delete(name);
|
||||
}
|
||||
})();
|
||||
@@ -159,7 +162,10 @@ export function installTool(name: string, opts: { notificationKey?: string } = {
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Cancels the install of `name`, queued or running (as the task's Cancel does). */
|
||||
export function cancelInstall(name: string) {
|
||||
const taskId = `tools:${name}`;
|
||||
if (useTasks.getState().tasks.some((t) => t.id === taskId)) return cancelTask(taskId);
|
||||
const job = useTools.getState().jobs[name];
|
||||
if (job) toolsBackend().cancel(job.request);
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача