дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(java): reject unusable language server JDKs (#2184)
An installed JDK that cannot run, such as an Intel-only JDK on Apple silicon, was registered as a project runtime and could crash Java language server initialization even with a working JDK selected. Each JDK's executable is now probed before selection, off the UI thread and outside the cache lock; unusable JDKs are left out, working Java 8 runtimes stay, and macOS bundle paths are accepted. An explicit choice that cannot run reports its actual executable. Since JDT keeps VM installs in its workspace, a project with an excluded JDK gets one fresh workspace; everyone else keeps their path. Fixes #1668
Цей коміт міститься в:
1 джерело
9f9a2d8cf0
коміт
a7a00da263
7 files changed
+311
-34
No files matched your search
@@ -134,21 +134,22 @@ pub async fn inspect_code(
|
||||
let cancel = host.begin_request(request);
|
||||
let listed = {
|
||||
let (project, cancel) = (project.clone(), cancel.clone());
|
||||
blocking(move || files_in_scope(&project, &scope, &cancel)).await
|
||||
blocking(move || {
|
||||
let files = files_in_scope(&project, &scope, &cancel)?;
|
||||
let total = files.len() as u32;
|
||||
let rel: std::collections::HashMap<_, _> =
|
||||
files.into_iter().filter(|f| project.lsp.serves_path(&f.path)).map(|f| (f.path, f.rel_path)).collect();
|
||||
Ok((total, rel))
|
||||
})
|
||||
.await
|
||||
};
|
||||
let files = match listed {
|
||||
let (total_in_scope, rel) = match listed {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
host.end_request(request);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let total_in_scope = files.len() as u32;
|
||||
let rel: std::collections::HashMap<_, _> = files
|
||||
.iter()
|
||||
.filter(|f| project.lsp.serves_path(&f.path))
|
||||
.map(|f| (f.path.clone(), f.rel_path.clone()))
|
||||
.collect();
|
||||
let mut summary = InspectSummary { skipped: total_in_scope - rel.len() as u32, ..Default::default() };
|
||||
let _ = on_event.send(InspectEvent::Started { total: rel.len() as u32 });
|
||||
|
||||
|
||||
@@ -170,7 +170,10 @@ pub fn jdtls_jdk(root: &Path, s: &Settings) -> Result<Option<crate::java::Jdk>,
|
||||
Some(Ok(jdk)) if !ide_core::java_runtime::supports_java_bundles(&jdk.home) => {
|
||||
Err(crate::java::server_jdk_incomplete_message(&jdk, jdk.major.max(crate::java::JDTLS_MIN_JAVA)))
|
||||
}
|
||||
Some(Ok(jdk)) => Ok(Some(jdk)),
|
||||
Some(Ok(jdk)) => match crate::java::java_execution_error(&crate::java::java_executable(&jdk.home)) {
|
||||
Some(error) => Err(error),
|
||||
None => Ok(Some(jdk)),
|
||||
},
|
||||
Some(Err(p)) => {
|
||||
Err(format!("The language server JDK {} (Settings › Languages & Frameworks › Java).", not_a_jdk(&p)))
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ pub fn parse_release(release: &str) -> Option<u32> {
|
||||
if first == 1 { parts.next()?.parse().ok() } else { Some(first) }
|
||||
}
|
||||
|
||||
/// The JDK at `home`, if it is one (a `release` file and `bin/java`).
|
||||
/// The JDK at `home` (or a macOS bundle), if it has a `release` file and `bin/java`.
|
||||
pub fn jdk_at(home: &Path) -> Option<Jdk> {
|
||||
let bundle_home = home.join("Contents/Home");
|
||||
let home = if bundle_home.is_dir() { bundle_home.as_path() } else { home };
|
||||
let release = std::fs::read_to_string(home.join("release")).ok()?;
|
||||
if !home.join("bin").join(crate::exe("java")).is_file() {
|
||||
return None;
|
||||
@@ -41,6 +43,69 @@ pub fn jdk_at(home: &Path) -> Option<Jdk> {
|
||||
Some(Jdk { major: parse_release(&release)?, home: crate::launch_path(home).unwrap_or_else(|_| home.to_path_buf()) })
|
||||
}
|
||||
|
||||
struct JavaProbe {
|
||||
modified: std::time::SystemTime,
|
||||
len: u64,
|
||||
permissions: std::fs::Permissions,
|
||||
at: Instant,
|
||||
runnable: bool,
|
||||
}
|
||||
|
||||
static JAVA_PROBES: Mutex<BTreeMap<PathBuf, JavaProbe>> = Mutex::new(BTreeMap::new());
|
||||
|
||||
/// Metadata can describe a JDK built for another architecture. Probe the executable
|
||||
/// before giving it to JDT, with a short cache shared by server and project selection.
|
||||
pub(crate) fn java_runs(java: &Path) -> bool {
|
||||
let Ok(metadata) = java.metadata() else { return false };
|
||||
let Ok(modified) = metadata.modified() else { return false };
|
||||
{
|
||||
let probes = JAVA_PROBES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(probe) = probes.get(java)
|
||||
&& probe.modified == modified
|
||||
&& probe.len == metadata.len()
|
||||
&& probe.permissions == metadata.permissions()
|
||||
&& probe.at.elapsed() < RESCAN_AFTER
|
||||
{
|
||||
return probe.runnable;
|
||||
}
|
||||
}
|
||||
let runnable = crate::exec::query(java, &["-version"], Duration::from_secs(5)).is_some();
|
||||
JAVA_PROBES.lock().unwrap_or_else(|e| e.into_inner()).insert(
|
||||
java.to_path_buf(),
|
||||
JavaProbe { modified, len: metadata.len(), permissions: metadata.permissions(), at: Instant::now(), runnable },
|
||||
);
|
||||
runnable
|
||||
}
|
||||
|
||||
fn project_jdk_is_usable(jdk: &Jdk) -> bool {
|
||||
jdk.home.join("bin").join(crate::exe("javac")).is_file()
|
||||
&& (jdk.major < 9 || ide_core::java_runtime::supports_java_bundles(&jdk.home))
|
||||
&& java_runs(&java_executable(&jdk.home))
|
||||
}
|
||||
|
||||
pub(crate) fn server_jdk_error(jdk: &Jdk, min_java: u32) -> Option<String> {
|
||||
server_jdk_metadata_error(jdk, min_java).or_else(|| java_execution_error(&java_executable(&jdk.home)))
|
||||
}
|
||||
|
||||
pub(crate) fn server_jdk_metadata_error(jdk: &Jdk, min_java: u32) -> Option<String> {
|
||||
if jdk.major < min_java {
|
||||
Some(server_jdk_too_old_message(jdk, min_java))
|
||||
} else if !ide_core::java_runtime::supports_java_bundles(&jdk.home) {
|
||||
Some(server_jdk_incomplete_message(jdk, min_java))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn java_execution_error(java: &Path) -> Option<String> {
|
||||
(!java_runs(java)).then(|| {
|
||||
format!(
|
||||
"The language server Java executable ({}) cannot run on this machine. Choose a working JDK in {SERVER_JDK_SETTING}, or set it to Auto.",
|
||||
java.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn children(dir: &Path) -> Vec<PathBuf> {
|
||||
let mut out: Vec<PathBuf> =
|
||||
std::fs::read_dir(dir).map(|rd| rd.flatten().map(|e| e.path()).collect()).unwrap_or_default();
|
||||
@@ -166,11 +231,14 @@ pub fn installed_jdks() -> Arc<[Jdk]> {
|
||||
/// the list is rescanned (at most every [`RESCAN_AFTER`]), so a JDK installed
|
||||
/// after startup is picked up when the server is restarted.
|
||||
pub fn jdtls_jdks(min_java: u32, data_dir: &Path) -> Arc<[Jdk]> {
|
||||
let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let installed = match &*slot {
|
||||
Some(s) if jdtls_runtime(&s.jdks, min_java).is_some() || s.at.elapsed() < RESCAN_AFTER => s.jdks.clone(),
|
||||
_ => rescan(&mut slot),
|
||||
let (installed, expired) = {
|
||||
let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match &*slot {
|
||||
Some(s) => (s.jdks.clone(), s.at.elapsed() >= RESCAN_AFTER),
|
||||
None => (rescan(&mut slot), false),
|
||||
}
|
||||
};
|
||||
let installed = if expired && jdtls_runtime(&installed, min_java).is_none() { rescan_jdks() } else { installed };
|
||||
runtime_candidates(&installed, data_dir).into()
|
||||
}
|
||||
|
||||
@@ -203,7 +271,7 @@ pub const JDTLS_MIN_JAVA: u32 = 21;
|
||||
|
||||
/// The JDK to run jdtls with: the first found that is new enough (`JAVA_HOME` wins).
|
||||
pub fn jdtls_runtime(jdks: &[Jdk], min_java: u32) -> Option<&Jdk> {
|
||||
jdks.iter().find(|j| j.major >= min_java && ide_core::java_runtime::supports_java_bundles(&j.home))
|
||||
jdks.iter().find(|j| server_jdk_error(j, min_java).is_none())
|
||||
}
|
||||
|
||||
/// The Java version the jdtls that `cfg` launches needs: the highest any of its
|
||||
@@ -290,7 +358,7 @@ fn manifest_min_java(manifest: &str) -> Option<u32> {
|
||||
/// jdtls itself runs on.
|
||||
pub fn runtimes(jdks: &[Jdk]) -> Vec<serde_json::Value> {
|
||||
let mut by_major: BTreeMap<u32, &Jdk> = BTreeMap::new();
|
||||
for j in jdks {
|
||||
for j in jdks.iter().filter(|jdk| project_jdk_is_usable(jdk)) {
|
||||
by_major.entry(j.major).or_insert(j);
|
||||
}
|
||||
by_major
|
||||
@@ -372,7 +440,8 @@ pub fn configure_jdtls(
|
||||
// A Lombok agent from the user's JVM arguments wins.
|
||||
let own_agent = crate::lombok::agent_in_args(cfg.args.iter().map(String::as_str));
|
||||
let agent = own_agent.as_deref().or(lombok);
|
||||
if !cfg.args.iter().any(|a| a == "-data") {
|
||||
let generated_workspace = !cfg.args.iter().any(|a| a == "-data");
|
||||
if generated_workspace {
|
||||
// ECJ builds incrementally: classes compiled without (or with another) Lombok
|
||||
// agent would be kept, so each agent gets its own workspace and a full build.
|
||||
let mut key = match agent {
|
||||
@@ -414,12 +483,22 @@ pub fn configure_jdtls(
|
||||
set_jvm_property(&mut cfg.args, ZIP64_LENIENCY);
|
||||
let managed = language_server_runtimes_dir(data_dir);
|
||||
let managed = dunce::canonicalize(&managed).unwrap_or(managed);
|
||||
let mut excluded_jdks = Vec::new();
|
||||
let project_jdks: Vec<_> = project_jdk
|
||||
.into_iter()
|
||||
.chain(jdks)
|
||||
.filter(|jdk| !jdk.home.starts_with(&managed) || project_jdk == Some(*jdk))
|
||||
.filter(|jdk| {
|
||||
if project_jdk_is_usable(jdk) {
|
||||
true
|
||||
} else {
|
||||
excluded_jdks.push(jdk.home.clone());
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let project_jdk = project_jdk.filter(|jdk| project_jdks.contains(jdk));
|
||||
let mut all: Vec<Jdk> = Vec::new();
|
||||
for j in project_jdk.into_iter().chain(jdks).chain(server_jdk) {
|
||||
if !all.contains(j) {
|
||||
@@ -427,13 +506,9 @@ pub fn configure_jdtls(
|
||||
}
|
||||
}
|
||||
let jdks = all.as_slice();
|
||||
if !cfg.env.contains_key("JAVA_HOME") {
|
||||
if cfg.launch_error.is_none() && !cfg.env.contains_key("JAVA_HOME") {
|
||||
let runtime = match server_jdk {
|
||||
Some(jdk) if jdk.major < min_java => Err(server_jdk_too_old_message(jdk, min_java)),
|
||||
Some(jdk) if !ide_core::java_runtime::supports_java_bundles(&jdk.home) => {
|
||||
Err(server_jdk_incomplete_message(jdk, min_java))
|
||||
}
|
||||
Some(jdk) => Ok(jdk),
|
||||
Some(jdk) => server_jdk_error(jdk, min_java).map_or(Ok(jdk), Err),
|
||||
None => jdtls_runtime(jdks, min_java).ok_or_else(|| missing_runtime_message(jdks, min_java)),
|
||||
};
|
||||
match runtime {
|
||||
@@ -442,10 +517,10 @@ pub fn configure_jdtls(
|
||||
}
|
||||
Err(e) => cfg.launch_error = Some(e),
|
||||
}
|
||||
} else if let Some(jdk) = cfg.env.get("JAVA_HOME").and_then(|home| jdk_at(Path::new(home)))
|
||||
&& !ide_core::java_runtime::supports_java_bundles(&jdk.home)
|
||||
} else if cfg.launch_error.is_none()
|
||||
&& let Some(jdk) = cfg.env.get("JAVA_HOME").and_then(|home| jdk_at(Path::new(home)))
|
||||
{
|
||||
cfg.launch_error = Some(server_jdk_incomplete_message(&jdk, min_java));
|
||||
cfg.launch_error = server_jdk_error(&jdk, min_java);
|
||||
}
|
||||
let mut runtimes = runtimes(&project_jdks);
|
||||
if let Some(project) = project_jdk.or_else(|| pick_jdk(&project_jdks, project_java_version(server_root))) {
|
||||
@@ -457,6 +532,14 @@ pub fn configure_jdtls(
|
||||
}
|
||||
}
|
||||
}
|
||||
if generated_workspace && !excluded_jdks.is_empty() {
|
||||
// JDT retains VM installs in its workspace even after they leave the runtime list.
|
||||
excluded_jdks.sort();
|
||||
excluded_jdks.dedup();
|
||||
let key = text_key(&json!(excluded_jdks).to_string());
|
||||
let data = cfg.args.iter().position(|a| a == "-data").unwrap() + 1;
|
||||
cfg.args[data].push_str(&format!("-runtimes-{key}"));
|
||||
}
|
||||
if !runtimes.is_empty() {
|
||||
merge_init_options(cfg, json!({ "settings": { "java": { "configuration": { "runtimes": runtimes } } } }));
|
||||
}
|
||||
@@ -587,6 +670,37 @@ fn parse_java_version(v: &str) -> Option<u32> {
|
||||
if first == 1 { parts.next()?.trim().parse().ok() } else { Some(first) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn write_test_java(home: &Path) {
|
||||
let java = java_executable(home);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::write(&java, "#!/bin/sh\necho 'openjdk version 25'\n").unwrap();
|
||||
std::fs::set_permissions(java, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
static BINARY: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new();
|
||||
let dir = BINARY.get_or_init(|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let source = dir.path().join("java.rs");
|
||||
std::fs::write(&source, "fn main() {}\n").unwrap();
|
||||
assert!(
|
||||
std::process::Command::new("rustc")
|
||||
.arg(source)
|
||||
.arg("-o")
|
||||
.arg(dir.path().join("java.exe"))
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
);
|
||||
dir
|
||||
});
|
||||
std::fs::copy(dir.path().join("java.exe"), java).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -599,7 +713,7 @@ mod tests {
|
||||
format!("JAVA_VERSION=\"{major}\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(java_executable(&home), "").unwrap();
|
||||
write_test_java(&home);
|
||||
std::fs::write(home.join("bin").join(crate::exe("javac")), "").unwrap();
|
||||
Jdk { major, home }
|
||||
}
|
||||
@@ -635,7 +749,7 @@ mod tests {
|
||||
"JAVA_VERSION=\"21.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(java_executable(&project_home), "").unwrap();
|
||||
write_test_java(&project_home);
|
||||
std::fs::write(project_home.join("bin").join(crate::exe("javac")), "").unwrap();
|
||||
let project = jdk_at(&project_home).unwrap();
|
||||
let discover = || jdk_homes(&project_root).filter_map(|home| jdk_at(&home)).collect::<Vec<_>>();
|
||||
@@ -649,7 +763,7 @@ mod tests {
|
||||
"JAVA_VERSION=\"25.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(java_executable(&home), "").unwrap();
|
||||
write_test_java(&home);
|
||||
std::fs::write(home.join("bin").join(crate::exe("javac")), "").unwrap();
|
||||
let candidates = runtime_candidates(&installed, data.path());
|
||||
assert_eq!(jdtls_runtime(&candidates, 25).unwrap().major, 25);
|
||||
@@ -730,6 +844,131 @@ mod tests {
|
||||
assert_eq!(parse_release("OTHER=1"), None);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn issue_1668_unrunnable_jdk_does_not_hide_valid_runtimes() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let broken = test_jdk(tmp.path(), 8, "jdk1.8.0_202.jdk/Contents/Home");
|
||||
std::fs::write(broken.home.join("release"), "JAVA_VERSION=\"1.8.0_202\"\n").unwrap();
|
||||
std::fs::write(java_executable(&broken.home), "#!/bin/sh\nexit 126\n").unwrap();
|
||||
std::fs::set_permissions(java_executable(&broken.home), std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let working = test_jdk(tmp.path(), 8, "working8");
|
||||
std::fs::write(working.home.join("release"), "JAVA_VERSION=\"1.8.0_372\"\n").unwrap();
|
||||
let server = test_jdk(tmp.path(), 25, "jdk25");
|
||||
let installed = [broken.clone(), working.clone(), server.clone()];
|
||||
for project in [None, Some(&broken)] {
|
||||
for explicit in [None, Some(&server)] {
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap();
|
||||
configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &installed, project, explicit, 25, &[], None);
|
||||
assert_eq!(cfg.launch_error, None);
|
||||
assert_eq!(cfg.env["JAVA_HOME"], server.home.to_string_lossy());
|
||||
let options = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
||||
let runtimes = options["settings"]["java"]["configuration"]["runtimes"].as_array().unwrap();
|
||||
assert_eq!(runtimes[0]["path"], json!(working.home));
|
||||
assert!(!runtimes.iter().any(|r| r["path"] == json!(broken.home)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn unusable_server_jdks_are_skipped_or_reported_without_losing_the_selected_path() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let server = test_jdk(tmp.path(), 25, "good25");
|
||||
for failure in ["exit126", "not-executable", "missing-javac", "jre", "timeout"] {
|
||||
let bad = test_jdk(tmp.path(), 25, failure);
|
||||
let java = java_executable(&bad.home);
|
||||
match failure {
|
||||
"exit126" => std::fs::write(&java, "#!/bin/sh\nexit 126\n").unwrap(),
|
||||
"not-executable" => std::fs::set_permissions(&java, std::fs::Permissions::from_mode(0o644)).unwrap(),
|
||||
"missing-javac" => std::fs::remove_file(bad.home.join("bin/javac")).unwrap(),
|
||||
"jre" => std::fs::write(bad.home.join("release"), "JAVA_VERSION=\"25\"\nIMAGE_TYPE=\"JRE\"\n").unwrap(),
|
||||
"timeout" => std::fs::write(&java, "#!/bin/sh\nexec /bin/sleep 10\n").unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let installed = [bad.clone(), server.clone()];
|
||||
assert_eq!(jdtls_runtime(&installed, 25), Some(&server), "{failure}");
|
||||
assert_eq!(runtimes(&installed)[0]["path"], json!(server.home), "{failure}");
|
||||
for env_override in [false, true] {
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap();
|
||||
if env_override {
|
||||
cfg.env.insert("JAVA_HOME".into(), bad.home.to_string_lossy().into_owned());
|
||||
}
|
||||
configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &installed, None, Some(&bad), 25, &[], None);
|
||||
let error = cfg.launch_error.unwrap();
|
||||
assert!(error.contains(bad.home.to_string_lossy().as_ref()), "{error}");
|
||||
assert!(!error.contains(server.home.to_string_lossy().as_ref()), "{error}");
|
||||
}
|
||||
write_test_java(&bad.home);
|
||||
if matches!(failure, "exit126" | "not-executable" | "timeout") {
|
||||
assert!(java_runs(&java), "a replaced or repaired executable is probed again");
|
||||
}
|
||||
}
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap();
|
||||
cfg.launch_error = Some("invalid selected JDK /outside/scan".into());
|
||||
configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &[], None, None, 25, &[], None);
|
||||
assert_eq!(cfg.launch_error.as_deref(), Some("invalid selected JDK /outside/scan"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn slow_java_probe_does_not_hold_the_cache_lock() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let jdk = test_jdk(tmp.path(), 25, "slow25");
|
||||
let ready = tmp.path().join("ready");
|
||||
std::fs::write(
|
||||
java_executable(&jdk.home),
|
||||
format!("#!/bin/sh\ntouch '{}'\nexec /bin/sleep 2\n", ready.display()),
|
||||
)
|
||||
.unwrap();
|
||||
std::thread::scope(|scope| {
|
||||
let probe = scope.spawn(|| java_runs(&java_executable(&jdk.home)));
|
||||
let started = Instant::now();
|
||||
while !ready.exists() && started.elapsed() < Duration::from_secs(5) {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(ready.exists(), "the probe must start");
|
||||
assert!(
|
||||
JAVA_PROBES.try_lock().is_ok(),
|
||||
"a running java -version must not lock out cached or unrelated probes"
|
||||
);
|
||||
assert!(probe.join().unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn only_excluded_jdks_change_the_generated_workspace() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project = test_jdk(tmp.path(), 8, "project8");
|
||||
let server = test_jdk(tmp.path(), 25, "server25");
|
||||
let other_server = test_jdk(tmp.path(), 26, "server26");
|
||||
let workspace = |jdks: &[Jdk], server: &Jdk, custom: bool| {
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap();
|
||||
if custom {
|
||||
cfg.args.extend(["-data".into(), "/own/workspace".into()]);
|
||||
}
|
||||
configure_jdtls(&mut cfg, tmp.path(), tmp.path(), jdks, Some(&project), Some(server), 25, &[], None);
|
||||
cfg.args.windows(2).find(|a| a[0] == "-data").unwrap()[1].clone()
|
||||
};
|
||||
let installed = [project.clone(), server.clone()];
|
||||
let before = workspace(&installed, &server, false);
|
||||
let original = tmp.path().join("lsp/jdtls").join(path_key(tmp.path()));
|
||||
assert_eq!(before, original.to_string_lossy(), "working JDKs retain the pre-existing workspace");
|
||||
assert_eq!(before, workspace(&[], &other_server, false), "installing or removing working JDKs keeps the path");
|
||||
std::fs::write(java_executable(&project.home), "#!/bin/sh\nexit 126\n").unwrap();
|
||||
let after = workspace(&installed, &server, false);
|
||||
assert_ne!(before, after, "the workspace holding the now-invalid JDK must not be reused");
|
||||
assert_eq!(after, workspace(&installed, &server, false));
|
||||
assert_eq!(after, workspace(std::slice::from_ref(&other_server), &other_server, false));
|
||||
assert_eq!(after, workspace(&[server.clone(), project.clone(), project.clone()], &server, false));
|
||||
assert_eq!(workspace(&installed, &server, true), "/own/workspace");
|
||||
write_test_java(&project.home);
|
||||
assert_eq!(before, workspace(&installed, &server, false), "repairing the JDK restores the original path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jdtls_workspace_and_runtimes() {
|
||||
let fixtures = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -292,8 +292,10 @@ pub fn launch_directly(cfg: &mut LanguageServerConfig) {
|
||||
}),
|
||||
};
|
||||
let Some((java, jdk)) = jdk.filter(|(java, _)| java.is_file()) else { return };
|
||||
if !ide_core::java_runtime::supports_java_bundles(&jdk.home) {
|
||||
cfg.launch_error = Some(crate::java::server_jdk_incomplete_message(&jdk, crate::java::jdtls_min_java(cfg)));
|
||||
if let Some(error) = crate::java::server_jdk_metadata_error(&jdk, crate::java::jdtls_min_java(cfg))
|
||||
.or_else(|| crate::java::java_execution_error(&java))
|
||||
{
|
||||
cfg.launch_error = Some(error);
|
||||
return;
|
||||
}
|
||||
cfg.args = java_args(&install, jdk.major, &cfg.args);
|
||||
@@ -526,7 +528,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(jdk.join("bin/javac"), "").unwrap();
|
||||
std::fs::write(jdk.join("bin/java"), "").unwrap();
|
||||
crate::java::write_test_java(&jdk);
|
||||
|
||||
let cfg = |command: &str| -> LanguageServerConfig {
|
||||
let mut cfg: LanguageServerConfig =
|
||||
@@ -555,6 +557,25 @@ mod tests {
|
||||
launch_directly(&mut overridden);
|
||||
assert!(overridden.launch_error.unwrap().contains("jdk.jdi"));
|
||||
|
||||
let alternate = jdk.join("bin/alternate-java");
|
||||
std::fs::write(&alternate, "#!/bin/sh\nexit 126\n").unwrap();
|
||||
std::fs::set_permissions(&alternate, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
|
||||
let mut overridden = cfg("jdtls");
|
||||
overridden.args.push(format!("--java-executable={}", alternate.display()));
|
||||
launch_directly(&mut overridden);
|
||||
let error = overridden.launch_error.unwrap();
|
||||
assert!(error.contains(alternate.to_string_lossy().as_ref()) && error.contains("cannot run"), "{error}");
|
||||
|
||||
let old = dir.path().join("jdk-17");
|
||||
std::fs::create_dir_all(old.join("bin")).unwrap();
|
||||
std::fs::write(old.join("release"), "JAVA_VERSION=\"17\"\n").unwrap();
|
||||
crate::java::write_test_java(&old);
|
||||
let mut overridden = cfg("jdtls");
|
||||
overridden.args.push(format!("--java-executable={}", old.join("bin/java").display()));
|
||||
launch_directly(&mut overridden);
|
||||
let error = overridden.launch_error.unwrap();
|
||||
assert!(error.contains(old.to_string_lossy().as_ref()) && error.contains("too old"), "{error}");
|
||||
|
||||
// Someone's own wrapper, or no usable JDK: the command is kept.
|
||||
std::fs::write(home.join("bin/my-jdtls"), "#!/bin/sh\n").unwrap();
|
||||
std::fs::set_permissions(home.join("bin/my-jdtls"), std::os::unix::fs::PermissionsExt::from_mode(0o755))
|
||||
@@ -637,7 +658,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(jdk.join("bin").join(crate::exe("javac")), "").unwrap();
|
||||
std::fs::write(jdk.join("bin").join(crate::exe("java")), "").unwrap();
|
||||
crate::java::write_test_java(&jdk);
|
||||
let target = format!(
|
||||
"path = \"C:\\Python313\\python.exe\"\nargs = {}\n",
|
||||
home.join("bin").join("jdtls").to_string_lossy()
|
||||
|
||||
@@ -313,7 +313,7 @@ fn npm_config_runs_on_its_own_node_interpreter() {
|
||||
|
||||
fn fake_jdk(dir: &Path) {
|
||||
std::fs::create_dir_all(dir.join("bin")).unwrap();
|
||||
std::fs::write(dir.join("bin").join(exe("java")), "").unwrap();
|
||||
crate::java::write_test_java(dir);
|
||||
std::fs::write(
|
||||
dir.join("release"),
|
||||
"JAVA_VERSION=\"17.0.9\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
@@ -921,6 +921,17 @@ fn jdtls_jdk_setting() {
|
||||
s.languages.java.jdtls_jdk = "jdk17.0.1".into();
|
||||
let err = jdtls_jdk(tmp.path(), &s).unwrap_err();
|
||||
assert!(err.contains("jdk17.0.1 does not exist") && err.contains("Languages & Frameworks › Java"), "{err}");
|
||||
|
||||
let bundle = tmp.path().join("outside-scan/jdk25.jdk");
|
||||
let home = bundle.join("Contents/Home");
|
||||
fake_jdk(&home);
|
||||
for path in [&bundle, &home, &home.join("bin"), &home.join("bin/java")] {
|
||||
s.languages.java.jdtls_jdk = path.to_string_lossy().into_owned();
|
||||
assert_eq!(jdtls_jdk(tmp.path(), &s).unwrap().unwrap().home, dunce::canonicalize(&home).unwrap());
|
||||
}
|
||||
std::fs::write(home.join("bin/java"), "#!/bin/sh\nexit 126\n").unwrap();
|
||||
let error = jdtls_jdk(tmp.path(), &s).unwrap_err();
|
||||
assert!(error.contains(home.to_string_lossy().as_ref()) && error.contains("cannot run"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"The language server Java executable ({path}) cannot run on this machine. Choose a working JDK in {setting}, or set it to Auto.": "The language server Java executable ({path}) cannot run on this machine. Choose a working JDK in {setting}, or set it to Auto.",
|
||||
"Input queue is full (4 MiB or 1024 writes); input was not sent.": "Input queue is full (4 MiB or 1024 writes); input was not sent.",
|
||||
"Cannot write to terminal": "Cannot write to terminal",
|
||||
"Docker": "Docker",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"The language server Java executable ({path}) cannot run on this machine. Choose a working JDK in {setting}, or set it to Auto.": "语言服务器的 Java 可执行文件({path})无法在此机器上运行。请在 {setting} 中选择可正常运行的 JDK,或设为自动。",
|
||||
"Input queue is full (4 MiB or 1024 writes); input was not sent.": "输入队列已满(4 MiB 或 1024 次写入);本次输入未发送。",
|
||||
"Cannot write to terminal": "无法写入终端",
|
||||
"Docker": "Docker",
|
||||
|
||||
Reference in new issue
Заблокувати користувача