дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-15 22:25:48 +02:00
Порівняти коміти
1
Коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
14674cab36 |
No files matched your search
@@ -445,8 +445,12 @@ impl Server {
|
||||
message.push_str(":\n");
|
||||
message.push_str(&tail);
|
||||
}
|
||||
let delay = self.record_crash();
|
||||
message.push_str(if delay.is_some() {
|
||||
// Starting the same build again can't help: only an update can.
|
||||
let expired = self.reported_expired_build();
|
||||
let delay = if expired { None } else { self.record_crash() };
|
||||
message.push_str(if expired {
|
||||
"\nThis build has expired; update it to start it again."
|
||||
} else if delay.is_some() {
|
||||
"\nRestarting…"
|
||||
} else {
|
||||
"\nCrashed repeatedly; restart it manually."
|
||||
@@ -757,6 +761,12 @@ impl Server {
|
||||
crash_excerpt(&lines, n)
|
||||
}
|
||||
|
||||
/// JetBrains preview builds (kotlin-lsp) refuse to run past their expiry date and
|
||||
/// exit with "This build of kotlin-server has expired".
|
||||
fn reported_expired_build(&self) -> bool {
|
||||
self.stderr.lock().iter().any(|l| is_expired_build_line(l))
|
||||
}
|
||||
|
||||
pub fn take_progress(&self) -> Vec<(TaskId, String)> {
|
||||
*self.loading.lock() = None;
|
||||
self.external_loading.store(0, std::sync::atomic::Ordering::Release);
|
||||
@@ -911,6 +921,10 @@ fn is_error_headline(line: &str) -> bool {
|
||||
named || l.starts_with("Exception in thread") || l.starts_with("thread '") && l.contains("panicked")
|
||||
}
|
||||
|
||||
fn is_expired_build_line(line: &str) -> bool {
|
||||
line.contains("This build of ") && line.contains(" has expired")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1071,6 +1085,31 @@ mod tests {
|
||||
assert_eq!(waiter.await.unwrap(), (true, ServerStatus::Crashed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_expired_build_is_not_restarted() {
|
||||
let sink: SharedSink = Arc::new(|_: IdeEvent| {});
|
||||
let s = server();
|
||||
let generation = s.begin_start().unwrap();
|
||||
s.push_stderr(generation, "idea.system.path=/tmp/idea-system".into());
|
||||
s.push_stderr(
|
||||
generation,
|
||||
"This build of kotlin-server has expired. The IDE will now close. Please download a new build".into(),
|
||||
);
|
||||
assert_eq!(s.on_closed(generation, &sink, Some(7)), Some(None));
|
||||
assert_eq!(s.status(), ServerStatus::Crashed);
|
||||
assert!(s.status_message().unwrap().ends_with("This build has expired; update it to start it again."));
|
||||
assert!(matches!(s.state.lock().conn, Conn::Failed));
|
||||
// A manual restart (after an update) starts it again.
|
||||
s.reset_for_restart();
|
||||
assert!(s.begin_start().is_some());
|
||||
|
||||
// Any other crash is restarted.
|
||||
let s = server();
|
||||
let generation = s.begin_start().unwrap();
|
||||
s.push_stderr(generation, "Exception in thread main".into());
|
||||
assert_eq!(s.on_closed(generation, &sink, Some(1)), Some(Some(Duration::from_millis(500))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_superseded_start_is_not_announced() {
|
||||
let s = server();
|
||||
|
||||
@@ -104,11 +104,14 @@ export async function restartServer(id: ServerId) {
|
||||
notifications().notify({ group: "lsp", level: "warning", title: tx`${entry.name} is still not installed` });
|
||||
return;
|
||||
}
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
// features/tools offers the update for a build that has expired.
|
||||
if (/This build of .+ has expired/.test(message) && getAction("tools.install")) return;
|
||||
notifications().notify({
|
||||
group: "lsp",
|
||||
level: "error",
|
||||
title: "Failed to restart language server",
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
body: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/** A language server build past its expiry date: offer the latest release instead of restarting it. */
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ToolInfo } from "@/bindings";
|
||||
import type { NotificationService } from "@/workbench/contributions";
|
||||
|
||||
const store = vi.hoisted(() => ({ tool: undefined as Partial<ToolInfo> | undefined, installTool: vi.fn() }));
|
||||
vi.mock("../store", async (orig) => ({
|
||||
...(await orig<typeof import("../store")>()),
|
||||
refreshTools: async () => {},
|
||||
toolByName: () => store.tool,
|
||||
installTool: store.installTool,
|
||||
}));
|
||||
|
||||
const { isExpiredBuild, promptUpdateExpired } = await import("../expired");
|
||||
const { provideNotificationService } = await import("@/workbench/contributions");
|
||||
|
||||
type Note = Parameters<NotificationService["notify"]>[0];
|
||||
let notes: Note[];
|
||||
const KOTLIN: Partial<ToolInfo> = {
|
||||
name: "kotlin-lsp",
|
||||
displayName: "Kotlin LSP (JetBrains)",
|
||||
command: "kotlin-lsp",
|
||||
canInstall: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
notes = [];
|
||||
provideNotificationService({ notify: (n) => void notes.push(n) });
|
||||
store.installTool.mockClear();
|
||||
});
|
||||
|
||||
describe("expired language server builds", () => {
|
||||
it("are recognised from the server's exit message", () => {
|
||||
expect(
|
||||
isExpiredBuild(
|
||||
"kotlin-lsp exited with code 7:\nThis build of kotlin-server has expired. The IDE will now close.\nThis build has expired; update it to start it again.",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isExpiredBuild("kotlin-lsp exited with code 1:\nOutOfMemoryError\nRestarting…")).toBe(false);
|
||||
expect(isExpiredBuild(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("offer an update of the IDE's own copy", async () => {
|
||||
store.tool = { ...KOTLIN, status: "installed" };
|
||||
await promptUpdateExpired("kotlin-lsp");
|
||||
const note = notes.at(-1)!;
|
||||
expect(note).toMatchObject({
|
||||
level: "warning",
|
||||
title: "Kotlin LSP (JetBrains) has expired",
|
||||
key: "lsp-expired-kotlin-lsp",
|
||||
});
|
||||
expect(note.actions?.map((a) => a.label)).toEqual(["Update", "Language Servers…"]);
|
||||
note.actions![0]!.run();
|
||||
expect(store.installTool).toHaveBeenCalledWith("kotlin-lsp", { notificationKey: "lsp-expired-kotlin-lsp" });
|
||||
});
|
||||
|
||||
it("offer the latest release for the IDE over one found on PATH", async () => {
|
||||
store.tool = { ...KOTLIN, status: "onPath", path: "/opt/homebrew/bin/kotlin-lsp" };
|
||||
await promptUpdateExpired("kotlin-lsp");
|
||||
const note = notes.at(-1)!;
|
||||
expect(note.body).toContain("/opt/homebrew/bin/kotlin-lsp");
|
||||
expect(note.actions?.map((a) => a.label)).toEqual(["Install Latest", "Language Servers…"]);
|
||||
});
|
||||
|
||||
it("point a chosen executable to the settings", async () => {
|
||||
store.tool = { ...KOTLIN, status: "custom", path: "/work/kotlin-lsp" };
|
||||
await promptUpdateExpired("kotlin-lsp");
|
||||
expect(notes.at(-1)!.actions?.map((a) => a.label)).toEqual(["Language Servers…"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* "kotlin-lsp has expired — Update": JetBrains preview builds stop starting once their date
|
||||
* passes, and the backend then stops restarting them. Offer the latest release instead.
|
||||
* A managed copy comes before `PATH` (Homebrew), so installing it also replaces one found there.
|
||||
*/
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { onIdeEvent } from "@/platform/ipc";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { installTool, refreshTools, SETTINGS_PAGE_ID, toolByName } from "./store";
|
||||
|
||||
export const isExpiredBuild = (message: string | null) => !!message && /This build of .+ has expired/.test(message);
|
||||
|
||||
export async function promptUpdateExpired(name: string) {
|
||||
await refreshTools();
|
||||
const tool = toolByName(name);
|
||||
const display = tool?.displayName ?? name;
|
||||
const key = `lsp-expired-${name}`;
|
||||
const installable = tool?.canInstall && (tool.status === "installed" || tool.status === "onPath");
|
||||
notifications().notify({
|
||||
group: "lsp",
|
||||
level: "warning",
|
||||
title: tx`${display} has expired`,
|
||||
body: !installable
|
||||
? tx`This build of ${display} no longer starts. Choose a newer build in Language Servers.`
|
||||
: tool.status === "installed"
|
||||
? tx`This build of ${display} no longer starts. Update it to the latest release?`
|
||||
: tx`This build of ${display} no longer starts. Install the latest release for the IDE? It is used instead of ${tool.path ?? tool.command}.`,
|
||||
key,
|
||||
sticky: true,
|
||||
actions: [
|
||||
...(installable
|
||||
? [
|
||||
{
|
||||
label: tool.status === "installed" ? "Update" : "Install Latest",
|
||||
run: () => void installTool(name, { notificationKey: key }),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: "Language Servers…", run: () => void executeAction("file.settings", SETTINGS_PAGE_ID) },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function installExpiredServerPrompts(): () => void {
|
||||
return onIdeEvent("languageServer", (e) => {
|
||||
if (e.status === "crashed" && isExpiredBuild(e.message)) void promptUpdateExpired(e.name);
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
import { lazyComponent } from "@/ui";
|
||||
import { executeAction, registerActions } from "@/platform/actions";
|
||||
import { registerSettingsPage } from "@/workbench/contributions";
|
||||
import { installExpiredServerPrompts } from "./expired";
|
||||
import { installJdkPrompts, registerJdkActions } from "./jdk";
|
||||
import { installMissingServerPrompts, installServerWarningPrompts, promptInstall } from "./missing";
|
||||
import { installTool, refreshTools, SETTINGS_PAGE_ID, toolByName, useTools } from "./store";
|
||||
@@ -85,6 +86,7 @@ export function activate(): () => void {
|
||||
]),
|
||||
installMissingServerPrompts(),
|
||||
installServerWarningPrompts(),
|
||||
installExpiredServerPrompts(),
|
||||
registerJdkActions(),
|
||||
installJdkPrompts(),
|
||||
];
|
||||
|
||||
@@ -1570,5 +1570,10 @@
|
||||
"Top right": "Top right",
|
||||
"Bottom left": "Bottom left",
|
||||
"Bottom right": "Bottom right",
|
||||
"Image files": "Image files"
|
||||
"Image files": "Image files",
|
||||
"{display} has expired": "{display} has expired",
|
||||
"This build of {display} no longer starts. Choose a newer build in Language Servers.": "This build of {display} no longer starts. Choose a newer build in Language Servers.",
|
||||
"This build of {display} no longer starts. Update it to the latest release?": "This build of {display} no longer starts. Update it to the latest release?",
|
||||
"This build of {display} no longer starts. Install the latest release for the IDE? It is used instead of {path}.": "This build of {display} no longer starts. Install the latest release for the IDE? It is used instead of {path}.",
|
||||
"Install Latest": "Install Latest"
|
||||
}
|
||||
@@ -7961,5 +7961,10 @@
|
||||
"Top right": "右上",
|
||||
"Bottom left": "左下",
|
||||
"Bottom right": "右下",
|
||||
"Image files": "图像文件"
|
||||
"Image files": "图像文件",
|
||||
"{display} has expired": "{display} 已过期",
|
||||
"This build of {display} no longer starts. Choose a newer build in Language Servers.": "此版本的 {display} 已无法启动。请在“语言服务器”中选择更新的版本。",
|
||||
"This build of {display} no longer starts. Update it to the latest release?": "此版本的 {display} 已无法启动。要更新到最新版本吗?",
|
||||
"This build of {display} no longer starts. Install the latest release for the IDE? It is used instead of {path}.": "此版本的 {display} 已无法启动。要为 IDE 安装最新版本吗?安装后将代替 {path} 使用。",
|
||||
"Install Latest": "安装最新版本"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача