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
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:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user