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
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
299 lines
10 KiB
Rust
299 lines
10 KiB
Rust
//! `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
|
||
}
|
||
}
|