mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(lsp): serialize the jdt.ls classpath resolve with the build lock (#946)
Running a main class in a multi-module Maven project could fail with "Cannot resolve the classpath: jdtls: Attempted to beginRule: P/<dependency>, does not match outer scope rule: P/<module>". `java.project.getClasspaths` and `getSettings` ran outside the jdt.ls build lock, so they could overlap a build or an m2e refresh of a sibling module that takes a nested Eclipse scheduling rule. The launch now resolves the classpath and JDK under the same build lock the build commands use. A delegated external build already holds that lock across the launch, so its callers pass `build_lock_held` instead of waiting on it again, which would deadlock. Fixes #933. Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com>
This commit is contained in:
co-authored by
a-rookie-of-C-language
parent
c50d212a1d
commit
2161a2ca03
@@ -59,7 +59,7 @@ async fn resolve_launch(
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
};
|
||||
let external_build = if delegated { Some(project.lsp.java_external_build(&module, cancel).await?) } else { None };
|
||||
let mut info = project.lsp.java_launch(&module, test, build && !delegated, cancel).await?;
|
||||
let mut info = project.lsp.java_launch(&module, test, build && !delegated, delegated, cancel).await?;
|
||||
let lsp_ms = started.elapsed().as_millis();
|
||||
let data_dir = host.local_data_dir.clone();
|
||||
let cancel = cancel.clone();
|
||||
|
||||
@@ -521,11 +521,15 @@ impl LspManager {
|
||||
}
|
||||
|
||||
/// Builds (when asked), then resolves the classpath and JDK of the module at `module`.
|
||||
/// The classpath queries take the module's workspace scheduling rule like the build
|
||||
/// commands do, so they run under the build lock; pass `build_lock_held` when the caller
|
||||
/// already holds it (the delegated external build), or the wait would deadlock.
|
||||
pub async fn java_launch(
|
||||
&self,
|
||||
module: &Path,
|
||||
test: bool,
|
||||
build: bool,
|
||||
build_lock_held: bool,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<JavaLaunchInfo> {
|
||||
let build = if build { Some(self.java_build(module, cancel).await?) } else { None };
|
||||
@@ -538,14 +542,23 @@ impl LspManager {
|
||||
}
|
||||
_ => (Vec::new(), 0),
|
||||
};
|
||||
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}");
|
||||
let (cp, java_home) = {
|
||||
let (server, _) = self.jdtls(module, cancel).await?;
|
||||
let _lock = if build_lock_held {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some(cancellable_wait(cancel, server.java_build_lock.lock()).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
|
||||
}
|
||||
};
|
||||
(cp, java_home)
|
||||
};
|
||||
Ok(JavaLaunchInfo {
|
||||
build,
|
||||
|
||||
@@ -1166,6 +1166,40 @@ async fn unmanaged_java_run_imports_sources_without_an_open_editor() {
|
||||
assert!(state.notified("textDocument/didOpen").is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn java_launch_serializes_the_classpath_with_the_build_lock() {
|
||||
let h = harness_for("java", "java", &[("jdtls", incremental_caps())], &[]);
|
||||
let (_, _file) = h.open("Main.java", "public class Main {}");
|
||||
h.wait_status("jdtls", ServerStatus::Ready).await;
|
||||
let state = h.launcher.state("jdtls");
|
||||
state.respond("java/buildWorkspace", json!(1));
|
||||
|
||||
// A launch with a build re-takes the lock after java_build has released it.
|
||||
let info = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
h.mgr.java_launch(&h.root, false, true, false, &Default::default()),
|
||||
)
|
||||
.await
|
||||
.expect("java_launch deadlocked on the build lock")
|
||||
.unwrap();
|
||||
assert!(info.class_paths.is_empty());
|
||||
|
||||
// The delegated external build holds the lock while java_launch resolves the
|
||||
// classpath; java_launch must not re-take it, or the launch would deadlock.
|
||||
let guard = h.mgr.java_external_build(&h.root, &Default::default()).await.unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(30), h.mgr.java_launch(&h.root, false, false, true, &Default::default()))
|
||||
.await
|
||||
.expect("java_launch deadlocked on the delegated external build's lock")
|
||||
.unwrap();
|
||||
drop(guard);
|
||||
let commands: Vec<_> = state
|
||||
.requested("workspace/executeCommand")
|
||||
.iter()
|
||||
.filter_map(|p| p["command"].as_str().map(str::to_owned))
|
||||
.collect();
|
||||
assert!(commands.contains(&"java.project.getClasspaths".to_owned()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn fallback_servers_only_run_when_the_preferred_one_is_missing() {
|
||||
// Preferred installed: the alternative is never started.
|
||||
|
||||
Reference in New Issue
Block a user