Files
openidea/crates/ide-jsonrpc/src/output.rs
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

137 lines
4.7 KiB
Rust

//! Text a server process writes outside the protocol: stderr, and stray stdout
//! lines (a launcher script's or the runtime's own messages) before a message.
//!
//! Such text is often not UTF-8: on Windows, `cmd.exe`, batch files and native
//! launchers write in the system code page (GBK on a Chinese system).
use encoding_rs::Encoding;
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
/// `bytes` as text: UTF-8 when valid, else (on Windows) the console / ANSI code
/// page, else UTF-8 with replacement characters.
pub fn decode(bytes: &[u8]) -> String {
decode_with(bytes, &native_code_pages())
}
/// [`decode`] trying the given Windows code pages, in order, after UTF-8.
pub fn decode_with(bytes: &[u8], code_pages: &[u32]) -> String {
if let Ok(s) = std::str::from_utf8(bytes) {
return s.to_owned();
}
for enc in code_pages.iter().filter_map(|&cp| encoding_for_code_page(cp)) {
let (text, had_errors) = enc.decode_without_bom_handling(bytes);
if !had_errors {
return text.into_owned();
}
}
String::from_utf8_lossy(bytes).into_owned()
}
/// The `encoding_rs` encoding of a Windows code page, when it has one.
pub fn encoding_for_code_page(cp: u32) -> Option<&'static Encoding> {
Some(match cp {
65001 => encoding_rs::UTF_8,
936 => encoding_rs::GBK,
54936 => encoding_rs::GB18030,
950 => encoding_rs::BIG5,
932 => encoding_rs::SHIFT_JIS,
949 => encoding_rs::EUC_KR,
866 => encoding_rs::IBM866,
874 => encoding_rs::WINDOWS_874,
1250 => encoding_rs::WINDOWS_1250,
1251 => encoding_rs::WINDOWS_1251,
1252 => encoding_rs::WINDOWS_1252,
1253 => encoding_rs::WINDOWS_1253,
1254 => encoding_rs::WINDOWS_1254,
1255 => encoding_rs::WINDOWS_1255,
1256 => encoding_rs::WINDOWS_1256,
1257 => encoding_rs::WINDOWS_1257,
1258 => encoding_rs::WINDOWS_1258,
20866 => encoding_rs::KOI8_R,
21866 => encoding_rs::KOI8_U,
_ => return None,
})
}
/// The OEM (console) and ANSI code pages: console programs and batch files write in
/// the former, GUI-subsystem and Java launchers in the latter.
#[cfg(windows)]
fn native_code_pages() -> [u32; 2] {
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetOEMCP() -> u32;
fn GetACP() -> u32;
}
// SAFETY: argument-less Win32 queries.
unsafe { [GetOEMCP(), GetACP()] }
}
#[cfg(not(windows))]
fn native_code_pages() -> [u32; 0] {
[]
}
/// Removes a trailing `\n` / `\r\n` (and stray `\r`s before it).
pub fn trim_line_end(line: &[u8]) -> &[u8] {
let mut end = line.len();
while end > 0 && matches!(line[end - 1], b'\n' | b'\r') {
end -= 1;
}
&line[..end]
}
/// Reads `stream` to its end, calling `on_line` with each line decoded by [`decode`].
/// Unlike `AsyncBufReadExt::lines`, a line that is not UTF-8 does not end the stream.
pub async fn read_lines<R: AsyncRead + Unpin>(stream: R, mut on_line: impl FnMut(String)) {
let mut reader = BufReader::new(stream);
let mut line = Vec::new();
loop {
line.clear();
match reader.read_until(b'\n', &mut line).await {
Ok(0) | Err(_) => break,
Ok(_) => on_line(decode(trim_line_end(&line))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `请按任意键继续. . .`, what `pause` prints on a Chinese Windows (code page 936).
const PAUSE_GBK: &[u8] = b"\xc7\xeb\xb0\xb4\xc8\xce\xd2\xe2\xbc\xfc\xbc\xcc\xd0\xf8. . .";
#[test]
fn utf8_passes_through() {
assert_eq!(decode_with("héllo 中文".as_bytes(), &[936]), "héllo 中文");
}
#[test]
fn code_page_text_is_decoded() {
assert_eq!(decode_with(PAUSE_GBK, &[936, 936]), "请按任意键继续. . .");
// The first code page that decodes cleanly wins.
assert_eq!(decode_with(PAUSE_GBK, &[437, 936]), "请按任意键继续. . .");
assert_eq!(decode_with(b"caf\xe9", &[1252]), "café");
}
#[test]
fn undecodable_text_is_lossy_utf8() {
let out = decode_with(PAUSE_GBK, &[]);
assert!(out.contains('\u{FFFD}') && out.ends_with(". . ."), "{out}");
assert_eq!(decode_with(b"a\xffb", &[65001]), "a\u{FFFD}b");
}
#[tokio::test]
async fn read_lines_survives_non_utf8_lines() {
let mut input = b"first\r\n".to_vec();
input.extend_from_slice(b"\xff\xfe broken\n");
input.extend_from_slice(b"last");
let mut lines = Vec::new();
read_lines(&input[..], |l| lines.push(l)).await;
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "first");
assert!(lines[1].ends_with(" broken"));
assert_eq!(lines[2], "last");
}
}