Files
Unlucky-Launcher/crates/core/src/diagnose/hs_err.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

299 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `hs_err_pid*.log`(JVM 致命错误日志)解析器。
//!
//! HMCL 完全不解析它(analyse/02 §7.10-8),而 `EXCEPTION_ACCESS_VIOLATION` 这类问题
//! 只有这份文件能给出权威证据(失败帧落在显卡驱动 DLL 里就是驱动问题)。
//! 格式稳定:**标题行 + 线程段 + 内存段**,本模块只做容错抽取,字段全部 `Option`,永不失败。
//!
//! 用法(launch 对接见 `CONTRACT.md` §5):
//! - `parse(&text)`:已有全文时用(首选,避免二次截断);
//! - `parse_file(&path)`:给路径时用;
//! - [`HsErrReport::digest`] 产出的摘要既是规则匹配的文本,也是给它人看的证据。
use std::fs;
use std::io;
use std::path::Path;
#[path = "hs_err_report.rs"]
mod report;
pub use report::{FailedFrame, FrameKind, HsErrKind, HsErrReport, MemorySummary};
/// 解析文本(永不失败;无法识别的行直接跳过)。
#[must_use]
pub fn parse(text: &str) -> HsErrReport {
let mut report = HsErrReport::default();
let lines = text.lines().collect::<Vec<_>>();
for (index, raw) in lines.iter().enumerate() {
let line = raw.trim_end_matches('\r');
let body = line.trim_start_matches('#').trim();
if report.header.is_none() && is_header(body) {
report.header = Some(body.to_owned());
}
if report.exception.is_none() {
report.exception = extract_exception(body);
}
if report.siginfo.is_none() && body.starts_with("siginfo:") {
report.siginfo = Some(body.to_owned());
}
if report.signal.is_none() {
report.signal = body
.contains("si_signo:")
.then(|| body.to_owned())
.or_else(|| extract_signal(body));
}
if report.jre_version.is_none() && body.contains("JRE version:") {
report.jre_version = Some(body.to_owned());
}
if report.java_vm.is_none() && body.contains("Java VM:") {
report.java_vm = Some(body.to_owned());
}
if report.os.is_none() && body.starts_with("OS:") {
report.os = Some(body.to_owned());
}
if report.command_line.is_none() && body.starts_with("java_command:") {
report.command_line = Some(body.to_owned());
}
if report.current_thread.is_none() && body.starts_with("Current thread (") {
report.current_thread = Some(body.to_owned());
}
if report.memory.is_none() && body.starts_with("Memory:") {
report.memory = parse_memory(body);
}
if report.failed_frame.is_none() && body.eq_ignore_ascii_case("Problematic frame:") {
report.failed_frame = lines
.iter()
.skip(index + 1)
.map(|next| next.trim_end_matches('\r'))
.find(|next| !next.trim().is_empty() && next.trim() != "#")
.map(|next| parse_frame(next.trim_start_matches('#').trim()));
}
if report.pc.is_none() && body.contains("pc=") {
report.pc = field(body, "pc=");
}
if report.pid.is_none() && body.contains("pid=") {
report.pid = field(body, "pid=").and_then(|raw| raw.parse::<u32>().ok());
}
if report.tid.is_none() && body.contains("tid=") {
report.tid = field(body, "tid=");
}
if body.eq_ignore_ascii_case("VM Arguments:") {
// 真实 hs_err 里 jvm_args/java_command 是顶格写的,不缩进
report.vm_arguments = collect_unindented(&lines, index + 1, 32);
}
if body.starts_with("Heap:") {
// "Heap:" 之后的缩进行才是内容(garbage-first heap total … used …)
report.heap = collect_block(&lines, index + 1, 6);
}
}
report.header_lines = collect_header_block(&lines);
report.pc = report
.exception
.as_deref()
.and_then(|line| field(line, "pc="))
.or(report.pc);
// pid/tid 在「异常行」和「Out of Memory Error (os_*.cpp:NNN), pid=…」两种标题里都出现,
// 所以两种形态都认,先到先得。
if report.pid.is_none() {
report.pid = report
.exception
.as_deref()
.and_then(|line| field(line, "pid="))
.and_then(|raw| raw.parse::<u32>().ok());
}
if report.tid.is_none() {
report.tid = report
.exception
.as_deref()
.and_then(|line| field(line, "tid="));
}
report.kind = classify(&report);
report
}
/// 读文件并解析(非 UTF-8 字节按 U+FFFD 替换,不报错)。
pub fn parse_file(path: impl AsRef<Path>) -> io::Result<HsErrReport> {
let path = path.as_ref();
let bytes = fs::read(path)?;
let text = String::from_utf8_lossy(&bytes);
let mut report = parse(&text);
report.path = Some(path.to_path_buf());
Ok(report)
}
fn is_header(body: &str) -> bool {
const MARKERS: [&str; 5] = [
"insufficient memory",
"A fatal error has been detected",
"Out of Memory Error",
"Internal Error",
"SIGSEGV",
];
MARKERS.iter().any(|marker| body.contains(marker))
}
fn extract_exception(body: &str) -> Option<String> {
let marker = "EXCEPTION_";
let start = body.find(marker)?;
let rest = &body[start..];
let end = rest
.find(" at pc=")
.or_else(|| rest.find(", pid="))
.unwrap_or(rest.len());
Some(rest[..end].trim().to_owned())
}
fn extract_signal(body: &str) -> Option<String> {
let start = body.find("SIG")?;
let rest = &body[start..];
let end = rest.find(')').map_or(rest.len(), |index| index + 1);
Some(rest[..end].trim().to_owned())
}
/// 从 `key=value` 形态里取值(到下一个空格/逗号为止)。
fn field(line: &str, key: &str) -> Option<String> {
let start = line.find(key)? + key.len();
let rest = &line[start..];
let end = rest.find([' ', ',', ']']).unwrap_or(rest.len());
let value = rest[..end].trim();
(!value.is_empty()).then(|| value.to_owned())
}
fn parse_frame(raw: &str) -> FailedFrame {
let kind = match raw.chars().next() {
Some('C') => FrameKind::Native,
Some('J' | 'j') => FrameKind::Java,
Some('V') => FrameKind::VmStub,
_ => FrameKind::Unknown,
};
let (module, offset) = raw
.find('[')
.and_then(|start| {
let inner = &raw[start + 1..];
let inner = &inner[..inner.find(']')?];
let mut parts = inner.split('+');
let module = parts.next().map(str::trim).filter(|m| !m.is_empty())?;
Some((
Some(module.to_owned()),
parts.next().map(|o| o.trim().to_owned()),
))
})
.unwrap_or((None, None));
FailedFrame {
raw: raw.to_owned(),
kind,
module,
offset,
}
}
fn parse_memory(body: &str) -> Option<MemorySummary> {
let mut summary = MemorySummary {
raw: body.to_owned(),
..MemorySummary::default()
};
for part in body.split(',') {
let part = part.trim();
if part.contains("page") {
summary.page_size = part
.trim_start_matches("Memory:")
.split_whitespace()
.next()
.map(|value| value.trim_end_matches(',').to_owned());
} else if part.starts_with("physical") {
let (total, free) = parse_kb_pair(part);
summary.physical_kb = total;
summary.physical_free_kb = free;
} else if part.starts_with("swap") {
let (total, free) = parse_kb_pair(part);
summary.swap_kb = total;
summary.swap_free_kb = free;
}
}
Some(summary)
}
/// `physical 16741480k(4716856k free)` → (16741480, 4716856)。
fn parse_kb_pair(part: &str) -> (Option<u64>, Option<u64>) {
let numbers = part
.split(|ch: char| !ch.is_ascii_digit())
.filter(|token| !token.is_empty())
.filter_map(|token| token.parse::<u64>().ok())
.collect::<Vec<_>>();
(numbers.first().copied(), numbers.get(1).copied())
}
/// 取从 `start` 开始、到第一个空行为止的连续行(VM Arguments / java_command 段)。
fn collect_unindented(lines: &[&str], start: usize, limit: usize) -> Vec<String> {
lines
.iter()
.skip(start)
.map(|line| line.trim_end_matches('\r'))
.take_while(|line| !line.trim().is_empty())
.take(limit)
.map(|line| line.trim().to_owned())
.collect()
}
/// 取从 `start` 开始、以空白/制表缩进的连续行(最多 `limit` 行)。
fn collect_block(lines: &[&str], start: usize, limit: usize) -> Vec<String> {
lines
.iter()
.skip(start)
.map(|line| line.trim_end_matches('\r'))
.skip_while(|line| line.trim().is_empty())
.take_while(|line| line.starts_with([' ', '\t']))
.take(limit)
.map(|line| line.trim().to_owned())
.collect()
}
/// 文件开头的 `#` 注释块(真实 hs_err 的标题/异常/失败帧都在这里)。
fn collect_header_block(lines: &[&str]) -> Vec<String> {
let mut out = Vec::new();
for line in lines {
let line = line.trim_end_matches('\r');
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
if trimmed.starts_with('#') {
out.push(trimmed.to_owned());
}
} else {
break;
}
if out.len() >= 64 {
break;
}
}
out
}
fn classify(report: &HsErrReport) -> HsErrKind {
let haystack = [
report.header.as_deref(),
report.exception.as_deref(),
report.siginfo.as_deref(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("\n");
if haystack.contains("EXCEPTION_ACCESS_VIOLATION") || haystack.contains("SIGSEGV") {
HsErrKind::AccessViolation
} else if haystack.contains("insufficient memory")
|| haystack.contains("Out of Memory")
|| haystack.contains("OutOfMemory")
{
HsErrKind::OutOfMemory
} else if haystack.contains("StackOverflow") || haystack.contains("stack overflow") {
HsErrKind::StackOverflow
} else if haystack.contains("Internal Error")
|| haystack.contains("A fatal error has been detected")
{
HsErrKind::InternalError
} else {
HsErrKind::Other
}
}