Файли
l0ng-ai a065189516 fix(lsp): Launch jdtls with java and surface output outside the protocol (#102)
On Windows the auto-downloaded jdtls ran through `bin\jdtls.bat`, which starts the Python wrapper and ends with `pause`. When Python failed, `pause` wrote its prompt to stdout in the console code page; the transport treated that as a broken LSP header ("non-UTF-8 header"), dropped it and the server was killed, so the status only said "exited with code 1".

- jdtls is now launched with `java` directly on every platform (Equinox launcher, platform config dir, JDK 24+ flags, forwarded JVM args, `-data`), so Python is no longer needed; user wrappers and unusual layouts keep their command.
- Output outside the protocol on stdout (and non-UTF-8 stderr) is decoded (UTF-8, then the Windows OEM/ANSI code page, then lossy), logged and included in the crash message, for stdio and TCP language servers, DAP adapters and ACP agents.
- Crash messages carry the real exit code and the last lines of output.

未在 Windows 实机验证 (Windows paths covered by unit tests and a Windows-target clippy run).

Fixes #90

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 20:44:09 +08:00

68 рядки
2.9 KiB
Rust

//! A stdio server that dies before the handshake: what it printed (on stderr, and
//! on stdout outside the protocol, in any encoding) must reach the crash message.
#![cfg(unix)]
use ide_core::event::ServerStatus;
use ide_core::{DocId, IdeEvent, LanguageId};
use ide_language::{LanguageConfig, LanguageRegistry, LanguageServerConfig};
use ide_lsp::{LspManager, ProcessLauncher};
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Duration;
/// Shaped like `jdtls.bat` on a Chinese Windows whose Python step fails: an error on
/// stderr, then `pause` prompting in GBK on stdout (`请按任意键继续. . .`), reading one
/// key from stdin, exiting with an error.
const LAUNCHER: &str = r#"#!/bin/sh
echo "'python' is not recognized as an internal or external command" >&2
printf '\307\353\260\264\310\316\322\342\274\374\274\314\320\370. . . '
head -c 1 >/dev/null
printf '\r\n'
exit 1
"#;
#[tokio::test(flavor = "multi_thread")]
async fn crash_before_the_handshake_reports_what_the_server_printed() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_path_buf();
let script = root.join("fake-jdtls");
std::fs::write(&script, LAUNCHER).unwrap();
std::fs::set_permissions(&script, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
let server: LanguageServerConfig =
toml::from_str(&format!("name = \"fake\"\ncommand = {:?}", script.to_string_lossy())).unwrap();
let lang: LanguageConfig = toml::from_str("id = \"java\"\nname = \"Java\"\nextensions = [\"java\"]").unwrap();
let lang = LanguageConfig { language_servers: vec![server], ..lang };
let registry = Arc::new(LanguageRegistry::from_configs(vec![lang]));
let events = Arc::new(Mutex::new(Vec::new()));
let sink = {
let events = events.clone();
Arc::new(move |e: IdeEvent| events.lock().push(e))
};
let mgr = LspManager::with_launcher(root.clone(), registry, sink, Arc::new(ProcessLauncher));
let path = root.join("A.java");
std::fs::write(&path, "class A {}\n").unwrap();
mgr.did_open(DocId::next(), &path, &LanguageId::new("java"), 1, "class A {}\n");
let crashed = || {
events.lock().iter().find_map(|e| match e {
IdeEvent::LanguageServer { status: ServerStatus::Crashed, message, .. } => message.clone(),
_ => None,
})
};
let mut message = None;
for _ in 0..1000 {
message = crashed();
if message.is_some() {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
let message = message.expect("the server is reported as crashed");
assert!(message.starts_with("fake exited with code 1:\n"), "{message}");
assert!(message.contains("'python' is not recognized"), "{message}");
// The stdout prompt, decoded (code page 936 on Windows, lossily elsewhere).
assert!(message.contains(". . ."), "{message}");
mgr.shutdown_all().await;
}