Unlucky Launcher v0.0.1-alpha
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 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
//! 崩溃现场扫描:`crash-reports/*.txt`、`hs_err_pid*.log`、`logs/latest.log`。
//!
//! 按 `DevDocs/SubAgentsTalk/launch-接口约定.md` §3:`crash_found` 事件的 `path` 是
//! **权威字段**(M15 按路径自己读盘,`excerpt` 仅供人读)。所以这里只做“找到并给出路径”,
//! 不做解析——解析(hs_err 的失败帧/内存段)是 M15 的 `hs_err` 解析器。
//!
//! 只收**本次会话之后**动过的文件(`since` = 进程启动时刻),避免把上一局的崩溃报告
//! 算到这一局头上(这是 HMCL 会犯的错)。
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
/// 崩溃现场文件来源。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrashSource {
/// `crash-reports/crash-*.txt`(游戏自己写的权威 Description + 栈)。
CrashReport,
/// JVM 致命错误 `hs_err_pid*.log`(HMCL 完全不解析的空白区)。
HsErr,
/// `logs/latest.log`(内存缓冲不够长时的兜底全文)。
LatestLog,
}
impl CrashSource {
/// 事件流里的字符串。
pub fn as_str(self) -> &'static str {
match self {
Self::CrashReport => "crash_report",
Self::HsErr => "hs_err",
Self::LatestLog => "latest_log",
}
}
}
/// 一个现场文件。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrashArtifact {
/// 来源类型。
pub source: CrashSource,
/// 绝对/相对路径(交给 M15)。
pub path: PathBuf,
/// 供人读的开头片段(`latest.log` 不给,太大)。见 `DevDocs/SubAgentsTalk/launch-接口约定.md` §4(写入前脱敏)。
pub excerpt: Option<String>,
}
/// 默认摘录上限(字符)。
pub const MAX_EXCERPT_CHARS: usize = 2000;
/// 扫描一次运行目录,返回按修改时间倒序的现场文件(最多 `limit` 个)。
///
/// `since` 之前的文件视为上一局遗留,忽略;给 `since` 留 1 秒容差以吸收文件系统时间精度。
pub fn scan(run_dir: &Path, since: SystemTime, limit: usize) -> Vec<CrashArtifact> {
let since = since.checked_sub(Duration::from_secs(1)).unwrap_or(since);
let mut found: Vec<(SystemTime, CrashArtifact)> = Vec::new();
collect_matching(
&run_dir.join("crash-reports"),
CrashSource::CrashReport,
since,
&mut found,
);
collect_matching(run_dir, CrashSource::HsErr, since, &mut found);
if let Some(artifact) = latest_log(run_dir, since) {
let modified = artifact_time(&artifact.path).unwrap_or(since);
found.push((modified, artifact));
}
found.sort_by_key(|item| std::cmp::Reverse(item.0));
found
.into_iter()
.take(limit)
.map(|(_, artifact)| artifact)
.collect()
}
fn collect_matching(
dir: &Path,
source: CrashSource,
since: SystemTime,
out: &mut Vec<(SystemTime, CrashArtifact)>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if !is_candidate(&path, source) {
continue;
}
let Some(modified) = artifact_time(&path) else {
continue;
};
if modified < since {
continue;
}
out.push((
modified,
CrashArtifact {
source,
path: path.clone(),
excerpt: read_excerpt(&path, MAX_EXCERPT_CHARS),
},
));
}
}
fn is_candidate(path: &Path, source: CrashSource) -> bool {
if !path.is_file() {
return false;
}
let name = path
.file_name()
.map(|name| name.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
match source {
CrashSource::CrashReport => name.ends_with(".txt"),
CrashSource::HsErr => name.starts_with("hs_err_pid") && name.ends_with(".log"),
CrashSource::LatestLog => false,
}
}
fn latest_log(run_dir: &Path, since: SystemTime) -> Option<CrashArtifact> {
let path = run_dir.join("logs").join("latest.log");
let modified = artifact_time(&path)?;
if modified < since {
return None;
}
Some(CrashArtifact {
source: CrashSource::LatestLog,
path,
excerpt: None,
})
}
fn artifact_time(path: &Path) -> Option<SystemTime> {
std::fs::metadata(path).ok()?.modified().ok()
}
/// 读文件开头 `max_chars` 个字符(读不到就 `None`,不因权限/编码问题中断收尾)。
pub fn read_excerpt(path: &Path, max_chars: usize) -> Option<String> {
let bytes = std::fs::read(path).ok()?;
let text = String::from_utf8_lossy(&bytes);
let mut excerpt = String::new();
for (count, ch) in text.chars().enumerate() {
if count >= max_chars {
excerpt.push('…');
break;
}
excerpt.push(ch);
}
Some(excerpt)
}