дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(lsp): allow long jdtls workspace builds and make Java launch resolution cancellable (#449)
java/buildWorkspace builds the whole imported jdtls workspace, and its 5-minute cap failed launches in large multi-module projects. The budget is now 20 minutes, and the timeout error names it so a real stall can be told apart. Waiting that long needs a way out, so java_resolve_launch now takes a RequestId and is cancellable locally and over remote. The token reaches the jdtls ready and import waits, the requests (sending $/cancelRequest), and the external build. Stopping a run session during this phase cancels silently and does not launch. Starting a debug session passes a request id but has no stop path yet. Closes #373 Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
1 джерело
14c7821cf4
коміт
d5fe771eed
8 files changed
+245
-39
No files matched your search
@@ -4,10 +4,11 @@ use crate::rpc::Dispatcher;
|
||||
|
||||
use super::build::load;
|
||||
use ide_build::compile;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult, RequestId};
|
||||
use ide_lsp::jdtls::JavaLaunchInfo;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Cancellable with `cancel_request(request)`.
|
||||
/// Builds the workspace (when `build`), then resolves the runtime (or test)
|
||||
/// classpath and the JDK of the module at `module` (absolute, or relative to the project root).
|
||||
/// A module with Kotlin / Groovy / Scala sources is built by its build system, whose class directories come first.
|
||||
@@ -17,17 +18,33 @@ pub async fn java_resolve_launch(
|
||||
module: PathBuf,
|
||||
test: bool,
|
||||
build: bool,
|
||||
request: RequestId,
|
||||
) -> IdeResult<JavaLaunchInfo> {
|
||||
let cancel = host.begin_request(request);
|
||||
let result = resolve_launch(host, window, module, test, build, &cancel).await;
|
||||
host.end_request(request);
|
||||
result
|
||||
}
|
||||
|
||||
async fn resolve_launch(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
module: PathBuf,
|
||||
test: bool,
|
||||
build: bool,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<JavaLaunchInfo> {
|
||||
let project = host.project(window)?;
|
||||
let module = project.root.join(module);
|
||||
let settings = project.settings.effective();
|
||||
// Let jdtls finish its build before the external compiler writes Kotlin/Scala classes.
|
||||
let mut info = project.lsp.java_launch(&module, test, build).await?;
|
||||
let mut info = project.lsp.java_launch(&module, test, build, cancel).await?;
|
||||
let built = if build && compile::has_non_java_sources(&module) {
|
||||
let (root, cache, module, settings) =
|
||||
(project.root.clone(), project.build_cache_dir(), module.clone(), settings.clone());
|
||||
let cancel = cancel.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
compile::build_for_launch(&root, &load(&root, &cache, &settings), &module, test, &CancellationToken::new())
|
||||
compile::build_for_launch(&root, &load(&root, &cache, &settings), &module, test, &cancel)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))??
|
||||
@@ -65,6 +82,9 @@ pub async fn java_resolve_launch(
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
info.hints.extend(lombok.problem());
|
||||
}
|
||||
if cancel.is_cancelled() {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
@@ -83,13 +103,14 @@ pub async fn java_build_workspace(
|
||||
) -> IdeResult<ide_lsp::jdtls::JavaBuildStatus> {
|
||||
let project = host.project(window)?;
|
||||
let module = project.root.join(module);
|
||||
project.lsp.java_build(&module).await
|
||||
project.lsp.java_build(&module, &CancellationToken::new()).await
|
||||
}
|
||||
|
||||
/// Commands of this module the remote server answers.
|
||||
pub(crate) fn register(d: &mut Dispatcher) {
|
||||
d.add("java_resolve_launch", |c| async move {
|
||||
java_resolve_launch(&c.host, &c.window, c.arg("module")?, c.arg("test")?, c.arg("build")?).await
|
||||
java_resolve_launch(&c.host, &c.window, c.arg("module")?, c.arg("test")?, c.arg("build")?, c.arg("request")?)
|
||||
.await
|
||||
});
|
||||
d.add("java_debug_server", |c| async move { java_debug_server(&c.host, &c.window, c.arg("module")?).await });
|
||||
d.add("java_build_workspace", |c| async move { java_build_workspace(&c.host, &c.window, c.arg("module")?).await });
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
use crate::LspManager;
|
||||
use crate::server::{Running, Server};
|
||||
use ide_core::error::ErrorKind;
|
||||
use ide_core::{IdeError, IdeResult, LanguageId};
|
||||
use ide_jsonrpc::RpcError;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult, LanguageId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use specta::Type;
|
||||
@@ -20,7 +19,9 @@ use std::time::{Duration, Instant};
|
||||
const SERVER: &str = "jdtls";
|
||||
/// First start imports the Maven / Gradle model, which can take minutes.
|
||||
const READY_TIMEOUT: Duration = Duration::from_secs(240);
|
||||
const BUILD_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
/// A workspace build of a large multi-module project can take far longer than
|
||||
/// an editor request; the user can always cancel the launch waiting on it.
|
||||
const BUILD_TIMEOUT: Duration = Duration::from_secs(20 * 60);
|
||||
const COMMAND_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
/// How long to retry while the project is still being imported.
|
||||
const IMPORT_WAIT: Duration = Duration::from_secs(90);
|
||||
@@ -163,10 +164,19 @@ fn missing_bundle(e: &IdeError) -> bool {
|
||||
m.contains("no delegatecommandhandler") || m.contains("unsupported command") || m.contains("not supported")
|
||||
}
|
||||
|
||||
/// Cancel local waits without interrupting a shared server's startup or import.
|
||||
async fn cancellable_wait<T>(cancel: &CancellationToken, wait: impl std::future::Future<Output = T>) -> IdeResult<T> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => Err(IdeError::cancelled()),
|
||||
result = wait => Ok(result),
|
||||
}
|
||||
}
|
||||
|
||||
impl LspManager {
|
||||
/// The jdtls instance serving `path` (a module directory or source file),
|
||||
/// started if needed and waited for.
|
||||
async fn jdtls(&self, path: &Path) -> IdeResult<(Arc<Server>, Running)> {
|
||||
async fn jdtls(&self, path: &Path, cancel: &CancellationToken) -> IdeResult<(Arc<Server>, Running)> {
|
||||
let java = LanguageId::new("java");
|
||||
let server = self
|
||||
.servers_for(&java, path)
|
||||
@@ -174,7 +184,7 @@ impl LspManager {
|
||||
.find(|s| s.name == SERVER)
|
||||
.ok_or_else(|| IdeError::unavailable("no Java language server (jdtls) is configured"))?;
|
||||
self.ensure_started(&server);
|
||||
match server.wait_ready(READY_TIMEOUT).await {
|
||||
match cancellable_wait(cancel, server.wait_ready(READY_TIMEOUT)).await? {
|
||||
Some(run) => Ok((server, run)),
|
||||
None => {
|
||||
let status = server.status();
|
||||
@@ -191,34 +201,64 @@ impl LspManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn jdtls_request(&self, path: &Path, method: &str, params: Value, timeout: Duration) -> IdeResult<Value> {
|
||||
let (server, run) = self.jdtls(path).await?;
|
||||
let fut = run.peer.request::<Value, Value>(method, params, None);
|
||||
async fn jdtls_request(
|
||||
&self,
|
||||
path: &Path,
|
||||
method: &str,
|
||||
params: Value,
|
||||
timeout: Duration,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<Value> {
|
||||
let (server, run) = self.jdtls(path, cancel).await?;
|
||||
let fut = run.peer.request::<Value, Value>(method, params, Some(cancel.clone()));
|
||||
match tokio::time::timeout(timeout, fut).await {
|
||||
Ok(r) => r.map_err(|e| crate::rpc_err(&server.name, method, e)),
|
||||
Err(_) => Err(crate::rpc_err(&server.name, method, RpcError::Timeout)),
|
||||
Err(_) => Err(IdeError::new(
|
||||
ErrorKind::Protocol,
|
||||
format!(
|
||||
"{SERVER}: {method} timed out after {} s; retry, or check that jdtls is not stuck",
|
||||
timeout.as_secs()
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `workspace/executeCommand` on the jdtls serving `path`.
|
||||
pub async fn jdtls_command(&self, path: &Path, command: &str, arguments: Vec<Value>) -> IdeResult<Value> {
|
||||
self.jdtls_command_cancellable(path, command, arguments, &CancellationToken::new()).await
|
||||
}
|
||||
|
||||
async fn jdtls_command_cancellable(
|
||||
&self,
|
||||
path: &Path,
|
||||
command: &str,
|
||||
arguments: Vec<Value>,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<Value> {
|
||||
self.jdtls_request(
|
||||
path,
|
||||
"workspace/executeCommand",
|
||||
json!({ "command": command, "arguments": arguments }),
|
||||
COMMAND_TIMEOUT,
|
||||
cancel,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Like [`jdtls_command`](Self::jdtls_command), retried while the project is still importing.
|
||||
async fn jdtls_project_command(&self, path: &Path, command: &str, arguments: Vec<Value>) -> IdeResult<Value> {
|
||||
async fn jdtls_project_command(
|
||||
&self,
|
||||
path: &Path,
|
||||
command: &str,
|
||||
arguments: Vec<Value>,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<Value> {
|
||||
let deadline = Instant::now() + IMPORT_WAIT;
|
||||
loop {
|
||||
match self.jdtls_command(path, command, arguments.clone()).await {
|
||||
match self.jdtls_command_cancellable(path, command, arguments.clone(), cancel).await {
|
||||
Err(e) if not_imported(&e) && Instant::now() < deadline => {
|
||||
tracing::debug!("{command}: {e}; waiting for the project import");
|
||||
tokio::time::sleep(Duration::from_millis(1500)).await;
|
||||
cancellable_wait(cancel, tokio::time::sleep(Duration::from_millis(1500))).await?;
|
||||
}
|
||||
r => return r,
|
||||
}
|
||||
@@ -245,43 +285,62 @@ impl LspManager {
|
||||
|
||||
/// Incremental build of the jdtls workspace (`java/buildWorkspace`), so
|
||||
/// class files are up to date before a launch.
|
||||
pub async fn java_build(&self, path: &Path) -> IdeResult<JavaBuildStatus> {
|
||||
let v = self.jdtls_request(path, "java/buildWorkspace", json!(false), BUILD_TIMEOUT).await?;
|
||||
pub async fn java_build(&self, path: &Path, cancel: &CancellationToken) -> IdeResult<JavaBuildStatus> {
|
||||
let v = self.jdtls_request(path, "java/buildWorkspace", json!(false), BUILD_TIMEOUT, cancel).await?;
|
||||
Ok(JavaBuildStatus::from_code(v.as_i64().unwrap_or(0)))
|
||||
}
|
||||
|
||||
/// Runtime (`test == false`) or test classpath of the project containing `path`.
|
||||
pub async fn java_classpath(&self, path: &Path, test: bool) -> IdeResult<JavaClasspath> {
|
||||
pub async fn java_classpath(
|
||||
&self,
|
||||
path: &Path,
|
||||
test: bool,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<JavaClasspath> {
|
||||
let uri = crate::uri::path_to_uri_string(path);
|
||||
let options = json!({ "scope": if test { "test" } else { "runtime" } }).to_string();
|
||||
let v =
|
||||
self.jdtls_project_command(path, "java.project.getClasspaths", vec![json!(uri), json!(options)]).await?;
|
||||
let v = self
|
||||
.jdtls_project_command(path, "java.project.getClasspaths", vec![json!(uri), json!(options)], cancel)
|
||||
.await?;
|
||||
Ok(parse_classpaths(&v))
|
||||
}
|
||||
|
||||
/// The JDK home jdtls uses for the project containing `path`.
|
||||
pub async fn java_vm_location(&self, path: &Path) -> IdeResult<Option<PathBuf>> {
|
||||
pub async fn java_vm_location(&self, path: &Path, cancel: &CancellationToken) -> IdeResult<Option<PathBuf>> {
|
||||
let uri = crate::uri::path_to_uri_string(path);
|
||||
let v = self
|
||||
.jdtls_project_command(
|
||||
path,
|
||||
"java.project.getSettings",
|
||||
vec![json!(uri), json!(["org.eclipse.jdt.ls.core.vm.location"])],
|
||||
cancel,
|
||||
)
|
||||
.await?;
|
||||
Ok(parse_vm_location(&v))
|
||||
}
|
||||
|
||||
/// Builds (when asked), then resolves the classpath and JDK of the module at `module`.
|
||||
pub async fn java_launch(&self, module: &Path, test: bool, build: bool) -> IdeResult<JavaLaunchInfo> {
|
||||
let build = if build { Some(self.java_build(module).await?) } else { None };
|
||||
pub async fn java_launch(
|
||||
&self,
|
||||
module: &Path,
|
||||
test: bool,
|
||||
build: bool,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<JavaLaunchInfo> {
|
||||
let build = if build { Some(self.java_build(module, cancel).await?) } else { None };
|
||||
if build == Some(JavaBuildStatus::Cancelled) {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
let (errors, error_count) = match build {
|
||||
Some(JavaBuildStatus::WithErrors | JavaBuildStatus::Failed) => self.java_compile_errors(module).await,
|
||||
Some(JavaBuildStatus::WithErrors | JavaBuildStatus::Failed) => {
|
||||
cancellable_wait(cancel, self.java_compile_errors(module)).await?
|
||||
}
|
||||
_ => (Vec::new(), 0),
|
||||
};
|
||||
let cp = self.java_classpath(module, test).await?;
|
||||
let java_home = match self.java_vm_location(module).await {
|
||||
let cp = self.java_classpath(module, test, cancel).await?;
|
||||
let java_home = match self.java_vm_location(module, cancel).await {
|
||||
Ok(home) => home,
|
||||
Err(e) if e.kind == ErrorKind::Cancelled => return Err(e),
|
||||
Err(e) => {
|
||||
tracing::debug!("java.project.getSettings: {e}");
|
||||
None
|
||||
@@ -303,7 +362,7 @@ impl LspManager {
|
||||
|
||||
/// Compile errors jdtls reports for its workspace (all modules it imported).
|
||||
async fn java_compile_errors(&self, module: &Path) -> (Vec<JavaCompileError>, u32) {
|
||||
let Ok((server, _)) = self.jdtls(module).await else { return (Vec::new(), 0) };
|
||||
let Ok((server, _)) = self.jdtls(module, &CancellationToken::new()).await else { return (Vec::new(), 0) };
|
||||
let collect = || compile_errors(server.diagnostics_snapshot(), MAX_LAUNCH_ERRORS);
|
||||
let found = collect();
|
||||
if found.1 > 0 {
|
||||
@@ -341,6 +400,20 @@ impl LspManager {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancellation_ends_a_pending_wait() {
|
||||
let cancel = CancellationToken::new();
|
||||
let waiting = cancellable_wait(&cancel, std::future::pending::<()>());
|
||||
let cancelling = async {
|
||||
tokio::task::yield_now().await;
|
||||
cancel.cancel();
|
||||
};
|
||||
let (result, ()) = tokio::time::timeout(Duration::from_secs(1), async { tokio::join!(waiting, cancelling) })
|
||||
.await
|
||||
.expect("cancelled wait must finish");
|
||||
assert_eq!(result.unwrap_err().kind, ErrorKind::Cancelled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_errors_are_sorted_and_capped() {
|
||||
let diag = |line: u32, severity, message: &str| lsp_types::Diagnostic {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//! IPC commands of `ide_host::ops::java` (the shared implementation).
|
||||
|
||||
use crate::state::AppState;
|
||||
use ide_core::IdeResult;
|
||||
use ide_core::{IdeResult, RequestId};
|
||||
#[allow(unused_imports)]
|
||||
pub use ide_host::ops::java::*;
|
||||
use ide_lsp::jdtls::JavaLaunchInfo;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{State, Window};
|
||||
|
||||
/// Cancellable with `cancel_request(request)`.
|
||||
/// Builds the workspace (when `build`), then resolves the runtime (or test)
|
||||
/// classpath and the JDK of the module at `module` (absolute, or relative to the project root).
|
||||
/// A module with Kotlin / Groovy / Scala sources is built by its build system, whose class directories come first.
|
||||
@@ -19,8 +20,9 @@ pub async fn java_resolve_launch(
|
||||
module: PathBuf,
|
||||
test: bool,
|
||||
build: bool,
|
||||
request: RequestId,
|
||||
) -> IdeResult<JavaLaunchInfo> {
|
||||
ide_host::ops::java::java_resolve_launch(&state.host, window.label(), module, test, build).await
|
||||
ide_host::ops::java::java_resolve_launch(&state.host, window.label(), module, test, build, request).await
|
||||
}
|
||||
|
||||
/// Port of java-debug's DAP server inside the jdtls serving `module` (started on demand).
|
||||
|
||||
@@ -606,11 +606,12 @@ export const commands = {
|
||||
*/
|
||||
buildRun: (kind: BuildKind, file: string | null, request: RequestId, clear: string[], onEvent: Channel<BuildEvent>) => typedError<BuildResult, IdeError>(__TAURI_INVOKE("build_run", { kind, file, request, clear, onEvent })),
|
||||
/**
|
||||
* Cancellable with `cancel_request(request)`.
|
||||
* Builds the workspace (when `build`), then resolves the runtime (or test)
|
||||
* classpath and the JDK of the module at `module` (absolute, or relative to the project root).
|
||||
* A module with Kotlin / Groovy / Scala sources is built by its build system, whose class directories come first.
|
||||
*/
|
||||
javaResolveLaunch: (module: string, test: boolean, build: boolean) => typedError<JavaLaunchInfo, IdeError>(__TAURI_INVOKE("java_resolve_launch", { module, test, build })),
|
||||
javaResolveLaunch: (module: string, test: boolean, build: boolean, request: RequestId) => typedError<JavaLaunchInfo, IdeError>(__TAURI_INVOKE("java_resolve_launch", { module, test, build, request })),
|
||||
/** Port of java-debug's DAP server inside the jdtls serving `module` (started on demand). */
|
||||
javaDebugServer: (module: string) => typedError<number, IdeError>(__TAURI_INVOKE("java_debug_server", { module })),
|
||||
/** Incremental build of the jdtls workspace serving `module` (e.g. before HotSwap). */
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("Java debugging (java-debug through jdtls)", () => {
|
||||
expect(isJavaDebuggable({ ...app, kind: "maven" })).toBe(false);
|
||||
const l = await javaLaunchConfig(app, "/p");
|
||||
expect(invoked).toEqual([
|
||||
["javaResolveLaunch", ["api", false, true]],
|
||||
["javaResolveLaunch", ["api", false, true, expect.any(Number)]],
|
||||
["javaDebugServer", ["api"]],
|
||||
]);
|
||||
expect(l).toMatchObject({ adapter: "java-debug", request: "launch", cwd: "/p/api", args: [], env: { A: "1" } });
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* JVMs started by Maven / Gradle with a JDWP agent are attached to.
|
||||
*/
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { call, commands, IpcError, type LaunchConfig, type RunConfig } from "@/platform/ipc";
|
||||
import { call, commands, IpcError, nextRequestId, type LaunchConfig, type RunConfig } from "@/platform/ipc";
|
||||
import {
|
||||
formatCompileErrors,
|
||||
isJvmApplication,
|
||||
@@ -76,7 +76,7 @@ export async function javaLaunchConfig(rc: RunConfig, root: string): Promise<Lau
|
||||
const cwd = rc.cwd ? join(root, rc.cwd) : root;
|
||||
const parsed = parseJavaArgs(rc.args);
|
||||
if (!parsed.mainClass) throw new Error(`No main class in '${rc.name}'`);
|
||||
const info = await call(commands.javaResolveLaunch(module, parsed.classpath === "test", true));
|
||||
const info = await call(commands.javaResolveLaunch(module, parsed.classpath === "test", true, nextRequestId()));
|
||||
if (info.errorCount > 0)
|
||||
notifications().notify({
|
||||
group: "run",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc";
|
||||
|
||||
const ipc = vi.hoisted(() => ({
|
||||
javaResolveLaunch: vi.fn(),
|
||||
cancelRequest: vi.fn(async () => undefined),
|
||||
runStart: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
return { ...real, isTauri: () => true, commands: { ...real.commands, ...ipc } };
|
||||
});
|
||||
vi.mock("@tauri-apps/api/core", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@tauri-apps/api/core")>()),
|
||||
Channel: class {
|
||||
onmessage: ((m: unknown) => void) | null = null;
|
||||
},
|
||||
}));
|
||||
|
||||
import { provideNotificationService } from "@/workbench/contributions";
|
||||
import { resetRuns, runtime, startRun, stopSession, useRun } from "./store";
|
||||
|
||||
const config: RunConfig = {
|
||||
id: "application:Main",
|
||||
name: "Main",
|
||||
kind: "application",
|
||||
command: "java",
|
||||
args: ["-cp", "${classpath}", "Main"],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: false,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
};
|
||||
const info: JavaLaunchInfo = {
|
||||
build: "succeeded",
|
||||
projectRoot: "/p",
|
||||
classPaths: ["/p/target/classes"],
|
||||
modulePaths: [],
|
||||
javaHome: null,
|
||||
javaExec: "java",
|
||||
shortenCommandLine: null,
|
||||
errors: [],
|
||||
errorCount: 0,
|
||||
hints: [],
|
||||
};
|
||||
const notify = vi.fn();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
provideNotificationService({ notify });
|
||||
});
|
||||
afterEach(() => resetRuns());
|
||||
|
||||
describe("Stop during Java launch resolution", () => {
|
||||
it.each(["cancelled", "success"])("cancels the request and ignores a late %s response", async (response) => {
|
||||
type Result = Awaited<ReturnType<typeof import("@/platform/ipc").commands.javaResolveLaunch>>;
|
||||
let resolve!: (result: Result) => void;
|
||||
const pending = new Promise<Result>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
ipc.javaResolveLaunch.mockReturnValue(pending);
|
||||
const launch = startRun(config);
|
||||
await vi.waitFor(() => expect(ipc.javaResolveLaunch).toHaveBeenCalledOnce());
|
||||
const session = useRun.getState().sessions[0]!;
|
||||
expect(session.status).toBe("starting");
|
||||
const request = ipc.javaResolveLaunch.mock.calls[0]![3];
|
||||
expect(request).toEqual(expect.any(Number));
|
||||
await stopSession(session.id);
|
||||
expect(ipc.cancelRequest).toHaveBeenCalledExactlyOnceWith(request);
|
||||
resolve(
|
||||
response === "cancelled"
|
||||
? { status: "error", error: { kind: "cancelled", message: "request cancelled" } }
|
||||
: { status: "ok", data: info },
|
||||
);
|
||||
await launch;
|
||||
expect(ipc.runStart).not.toHaveBeenCalled();
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
expect(runtime(session.id)!.console.lines.some((line) => line.kind === "stderr")).toBe(false);
|
||||
expect(useRun.getState().sessions[0]).toMatchObject({ stopped: true, status: "finished", runId: null });
|
||||
expect(runtime(session.id)!.resolveRequest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
call,
|
||||
commands,
|
||||
isTauri,
|
||||
isCancelled,
|
||||
nextRequestId,
|
||||
type RequestId,
|
||||
type OutputChunk,
|
||||
type RunConfig,
|
||||
type RunId,
|
||||
@@ -68,6 +71,7 @@ export interface RunSession {
|
||||
}
|
||||
|
||||
interface SessionRuntime {
|
||||
resolveRequest?: { id: RequestId; cancelled: boolean };
|
||||
console: ConsoleModel;
|
||||
tests: TestResults | null;
|
||||
parser: TestOutputParser | null;
|
||||
@@ -620,7 +624,10 @@ export async function startRun(
|
||||
const resolved = await resolveJvmApplication(sessionId, launch);
|
||||
if (runtimes.get(sessionId)?.generation !== generation) return getSession(sessionId) ?? null;
|
||||
if (!resolved) {
|
||||
patchSession(sessionId, { status: "failed", endedAt: Date.now() });
|
||||
patchSession(sessionId, {
|
||||
status: getSession(sessionId)?.stopped ? "finished" : "failed",
|
||||
endedAt: Date.now(),
|
||||
});
|
||||
return getSession(sessionId) ?? null;
|
||||
}
|
||||
launch = resolved;
|
||||
@@ -690,8 +697,14 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
|
||||
}
|
||||
if (!parsed.classpath) return config;
|
||||
say(`Building and resolving the classpath of ${parsed.mainClass}…\n`);
|
||||
const rt = runtime(sessionId)!;
|
||||
const request = { id: nextRequestId(), cancelled: false };
|
||||
rt.resolveRequest = request;
|
||||
try {
|
||||
const info = await call(commands.javaResolveLaunch(config.cwd ?? "", parsed.classpath === "test", true));
|
||||
const info = await call(
|
||||
commands.javaResolveLaunch(config.cwd ?? "", parsed.classpath === "test", true, request.id),
|
||||
);
|
||||
if (request.cancelled) return null;
|
||||
if (info.build === "withErrors")
|
||||
say("Build completed with errors; launching with the last compiled classes.\n", "stderr");
|
||||
if (info.build === "failed") say("Build failed; launching with the last compiled classes.\n", "stderr");
|
||||
@@ -715,6 +728,10 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
|
||||
return resolveJavaCommand(config, info, pathSeparator());
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
if (request.cancelled || isCancelled(e)) {
|
||||
if (runtimes.get(sessionId) === rt && rt.resolveRequest === request) patchSession(sessionId, { stopped: true });
|
||||
return null;
|
||||
}
|
||||
say(`Cannot resolve the classpath: ${message}\n`, "stderr");
|
||||
// jdtls can't start without a newer JDK: "Download JDK 25".
|
||||
const actions = downloadJdkActions(message);
|
||||
@@ -726,6 +743,8 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom
|
||||
actions,
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
if (rt.resolveRequest === request) delete rt.resolveRequest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,7 +854,15 @@ export function onRunExited(runId: RunId, code: number | null) {
|
||||
|
||||
export async function stopSession(sessionId: string): Promise<void> {
|
||||
const session = getSession(sessionId);
|
||||
if (!session || session.runId === null || session.status !== "running") return;
|
||||
if (!session) return;
|
||||
const request = runtimes.get(sessionId)?.resolveRequest;
|
||||
if (session.status === "starting" && request) {
|
||||
request.cancelled = true;
|
||||
patchSession(sessionId, { stopped: true });
|
||||
await commands.cancelRequest(request.id);
|
||||
return;
|
||||
}
|
||||
if (session.runId === null || session.status !== "running") return;
|
||||
patchSession(sessionId, { stopped: true });
|
||||
try {
|
||||
await call(commands.runStop(session.runId));
|
||||
@@ -995,7 +1022,7 @@ export async function closeSessionTab(sessionId: string): Promise<boolean> {
|
||||
export function closeSession(sessionId: string) {
|
||||
const session = getSession(sessionId);
|
||||
if (!session) return;
|
||||
if (session.status === "running") void stopSession(sessionId);
|
||||
if (session.status === "running" || session.status === "starting") void stopSession(sessionId);
|
||||
const rt = runtimes.get(sessionId);
|
||||
if (rt) cancelFlush(rt);
|
||||
runtimes.delete(sessionId);
|
||||
|
||||
Reference in new issue
Заблокувати користувача