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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-09 03:21:47 +08:00
1 джерело 9f9a2d8cf0
коміт a7a00da263
7 files changed
+311 -34

No files matched your search

+9 -8
Переглянути файл
@@ -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 });
+4 -1
Переглянути файл
@@ -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)))
}
+259 -20
Переглянути файл
@@ -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();
+25 -4
Переглянути файл
@@ -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()
+12 -1
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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",