fix(java): pass the IDE proxy to language-server JVMs (#745)

A JVM reads none of HTTP_PROXY/HTTPS_PROXY, so jdtls went direct and a repository that accepts the connection but never answers stalled the Maven import. ProxySettings::jvm_args turns the proxy configuration into JVM system properties (useSystemProxies for Auto, http/https/nonProxyHosts for a manual HTTP proxy, socks* for SOCKS), passed to jdtls as --jvm-arg; a property the user set themselves (matched by exact name) wins. Maven sync keeps Maven's normal snapshot update policy.

Co-authored-by: misswell <[email protected]>
Цей коміт міститься в:
misswellтаmisswell authored and GitHub committed 2026-11-02 10:00:32 +08:00
1 джерело e7f00ee7ce
коміт 2653021389
2 files changed
+126

No files matched your search

+80
Переглянути файл
@@ -207,6 +207,58 @@ impl ProxySettings {
env.push((OsString::from("no_proxy"), Some(OsString::from(no_proxy))));
env
}
/// System properties that make a JVM follow this configuration. A JVM reads none of
/// [`Self::child_env`]: a Java tool (jetty, Maven's resolver, a language server's own
/// HTTP client) goes direct without these, and a repository its proxy answers in
/// milliseconds can then hang it instead.
pub fn jvm_args(&self) -> Vec<String> {
match self.mode {
// macOS and Windows JVMs read the system proxy with this one.
ProxyMode::Auto => vec!["-Djava.net.useSystemProxies=true".into()],
ProxyMode::None => Vec::new(),
ProxyMode::Manual if self.manual() => self.manual_jvm_args(),
ProxyMode::Manual => Vec::new(),
}
}
fn manual_jvm_args(&self) -> Vec<String> {
let (host, port) = (self.host.trim(), self.port);
match self.kind {
ProxyKind::Http => {
// HTTPS hosts read `https.proxyHost`, plain ones `http.proxyHost`.
let mut args = vec![
format!("-Dhttp.proxyHost={host}"),
format!("-Dhttp.proxyPort={port}"),
format!("-Dhttps.proxyHost={host}"),
format!("-Dhttps.proxyPort={port}"),
];
let hosts = self.non_proxy_hosts();
if !hosts.is_empty() {
args.push(format!("-Dhttp.nonProxyHosts={}", hosts.join("|")));
}
args
}
// SOCKS proxies every TCP connection, so there is one property pair.
ProxyKind::Socks => vec![
format!("-DsocksProxyHost={host}"),
format!("-DsocksProxyPort={port}"),
"-Djava.net.useSystemProxies=false".into(),
],
}
}
/// [`Self::no_proxy`] in the JVM's `http.nonProxyHosts` format: `|` between host
/// globs. CIDR ranges and IPv6 literals have no equivalent there and are dropped.
fn non_proxy_hosts(&self) -> Vec<String> {
let mut hosts: Vec<String> = LOOPBACK.iter().filter(|h| !h.contains(':')).map(|h| (*h).to_owned()).collect();
for entry in self.no_proxy_entries() {
if !entry.contains(['/', ':', '[', ']']) && !hosts.contains(&entry) {
hosts.push(entry);
}
}
hosts
}
}
/// Case-insensitive host glob (`pattern` is lowercase): `*` matches any run of characters.
@@ -292,6 +344,34 @@ mod tests {
assert!(p.child_env().is_empty());
}
#[test]
fn jvm_args_carry_the_proxy_as_system_properties() {
// A JVM reads none of `child_env`: a Java language server needs these to not go
// direct while curl, git and Node follow the environment.
assert_eq!(ProxySettings::default().jvm_args(), ["-Djava.net.useSystemProxies=true"]);
assert!(ProxySettings { mode: ProxyMode::None, ..manual("proxy", 3128) }.jvm_args().is_empty());
assert_eq!(
manual("proxy.corp", 3128).jvm_args(),
[
"-Dhttp.proxyHost=proxy.corp",
"-Dhttp.proxyPort=3128",
"-Dhttps.proxyHost=proxy.corp",
"-Dhttps.proxyPort=3128",
"-Dhttp.nonProxyHosts=localhost|127.0.0.1",
]
);
let socks = ProxySettings { kind: ProxyKind::Socks, ..manual("10.0.0.1", 1080) };
assert_eq!(socks.jvm_args()[..2], ["-DsocksProxyHost=10.0.0.1", "-DsocksProxyPort=1080"]);
// `http.nonProxyHosts` takes host globs: CIDR ranges and IPv6 literals drop out.
let p = ProxySettings {
no_proxy: "*.corp.com, 172.0.0.0/8, [::1], ::1, localhost".into(),
..manual("proxy.corp", 3128)
};
assert_eq!(p.jvm_args().last().unwrap(), "-Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.com");
// A manual proxy without a host connects directly, and so does its JVM.
assert!(manual(" ", 80).jvm_args().is_empty());
}
#[test]
fn manual_http_and_socks_urls() {
let p = manual("proxy.corp", 3128);
+46
Переглянути файл
@@ -313,6 +313,19 @@ fn text_key(text: &str) -> String {
format!("{h:016x}")
}
fn add_jdtls_proxy_args(args: &mut Vec<String>, proxy: &ide_core::proxy_settings::ProxySettings) {
for arg in proxy.jvm_args() {
let key = arg.split('=').next().unwrap_or(arg.as_str());
if !args.iter().enumerate().any(|(i, a)| {
let value =
a.strip_prefix("--jvm-arg=").or_else(|| (i > 0 && args[i - 1] == "--jvm-arg").then_some(a.as_str()));
value.is_some_and(|a| a.split('=').next() == Some(key))
}) {
args.push(format!("--jvm-arg={arg}"));
}
}
}
const METADATA_PROPERTY: &str = "java.import.generatesMetadataFilesAtProjectRoot";
/// jdtls: its Eclipse workspace (`-data`) lives under the IDE's data
@@ -367,6 +380,10 @@ pub fn configure_jdtls(
cfg.args.push(format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy()));
}
crate::jdtls::add_default_vm_args(&mut cfg.args);
// jdtls resolves Maven dependencies through its own HTTP client, and a JVM reads none
// of the `HTTP_PROXY` / `HTTPS_PROXY` it inherits: pass the same proxy as system
// properties. One the user set in their own JVM arguments wins.
add_jdtls_proxy_args(&mut cfg.args, &ide_core::proxy_settings::proxy());
let managed = language_server_runtimes_dir(data_dir);
let managed = dunce::canonicalize(&managed).unwrap_or(managed);
let project_jdks: Vec<_> = project_jdk
@@ -705,6 +722,35 @@ mod tests {
assert!(other.args.is_empty() && other.initialization_options.is_none());
}
#[test]
fn jdtls_proxy_properties_respect_exact_user_overrides() {
use ide_core::proxy_settings::{ProxyMode, ProxySettings};
let proxy =
ProxySettings { mode: ProxyMode::Manual, host: "ide.proxy".into(), port: 3128, ..Default::default() };
for (user_arg, overridden) in [
("--jvm-arg=-Dhttp.proxyHost=user.proxy", true),
("--jvm-arg=-Dhttp.proxyHost", true),
("--jvm-arg=-Dhttp.proxyHostSuffix=unrelated", false),
("--jvm-arg=-Dother=-Dhttp.proxyHost", false),
("/workspace/-Dhttp.proxyHost", false),
] {
let mut forms = vec![vec![user_arg.to_owned()]];
if let Some(value) = user_arg.strip_prefix("--jvm-arg=") {
forms.push(vec!["--jvm-arg".into(), value.into()]);
}
for mut args in forms {
let user_args = args.clone();
add_jdtls_proxy_args(&mut args, &proxy);
assert_eq!(args[..user_args.len()], user_args);
assert_eq!(args.contains(&"--jvm-arg=-Dhttp.proxyHost=ide.proxy".into()), !overridden, "{user_arg}");
assert!(args.contains(&"--jvm-arg=-Dhttps.proxyHost=ide.proxy".into()));
let before = args.clone();
add_jdtls_proxy_args(&mut args, &proxy);
assert_eq!(args, before, "idempotent: {user_arg}");
}
}
}
#[test]
fn jdtls_lombok_agent() {
let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };