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

+299
View File
@@ -0,0 +1,299 @@
//! 五类信号的具体采集器(推送类 2 个 + 磁盘类 3 个)。
//!
//! 由 `collectors.rs` 用 `#[path]` 声明并统一导出;拆分只为守单文件 400 行上限。
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::diagnose::hs_err;
use super::{
CapturedText, CollectError, CollectedSources, CollectionContext, EnvironmentSnapshot,
ExitSignal, SignalBundle, SignalCollector, SignalKind, SignalOrigin,
};
/// 推送类:退出信号(launch 的 `exit` 事件已算好)。
#[derive(Debug, Clone)]
pub struct ExitCollector {
signal: ExitSignal,
}
impl ExitCollector {
/// 构造。
#[must_use]
pub fn new(signal: ExitSignal) -> Self {
Self { signal }
}
}
impl SignalCollector for ExitCollector {
fn kind(&self) -> SignalKind {
SignalKind::Exit
}
fn collect(
&self,
_ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
out.exit = Some(self.signal.clone());
Ok(CollectedSources::new())
}
}
/// 推送类:环境快照 + mod 清单(launch/game 侧已有数据)。
#[derive(Debug, Clone, Default)]
pub struct EnvironmentCollector {
snapshot: EnvironmentSnapshot,
}
impl EnvironmentCollector {
/// 构造。
#[must_use]
pub fn new(snapshot: EnvironmentSnapshot) -> Self {
Self { snapshot }
}
}
impl SignalCollector for EnvironmentCollector {
fn kind(&self) -> SignalKind {
SignalKind::Environment
}
fn collect(
&self,
_ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
out.mods = self.snapshot.mods.clone();
out.environment = Some(self.snapshot.clone());
Ok(CollectedSources::new())
}
}
/// 磁盘类:`crash-reports/crash-*.txt`(双通道里的「权威全文」)。
#[derive(Debug, Clone, Default)]
pub struct CrashReportCollector {
dir: Option<PathBuf>,
}
impl CrashReportCollector {
/// 默认扫 `<run_dir>/crash-reports`。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 指定目录。
#[must_use]
pub fn with_dir(dir: impl Into<PathBuf>) -> Self {
Self {
dir: Some(dir.into()),
}
}
}
impl SignalCollector for CrashReportCollector {
fn kind(&self) -> SignalKind {
SignalKind::CrashReport
}
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
let dir = self
.dir
.clone()
.unwrap_or_else(|| ctx.run_dir.join("crash-reports"));
let Some(path) = newest_file(&dir, ctx.started_at, |name| {
name.starts_with("crash-") && name.ends_with(".txt")
}) else {
return Ok(CollectedSources::new());
};
let text = read_lossy(SignalKind::CrashReport, &path)?;
let modified = modified_at(&path);
let mut sources = CollectedSources::new();
sources.push(&path);
out.sources.push(path.clone());
out.crash_report = Some(CapturedText {
path,
text,
modified,
});
Ok(sources)
}
}
/// 磁盘类:`hs_err_pid*.log`(HMCL 的空白区,显卡驱动类崩溃的唯一权威证据)。
#[derive(Debug, Clone, Default)]
pub struct HsErrCollector {
dirs: Vec<PathBuf>,
}
impl HsErrCollector {
/// 默认扫 `<run_dir>`(JVM 把 hs_err 写在进程工作目录)。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 指定候选目录表(**整表替换**,不是追加;给出后不再自动扫 `run_dir`)。
///
/// 典型用法:`HsErrCollector::with_dirs([run_dir.clone(), home_dir])`。
#[must_use]
pub fn with_dirs(dirs: impl IntoIterator<Item = PathBuf>) -> Self {
Self {
dirs: dirs.into_iter().collect(),
}
}
}
impl SignalCollector for HsErrCollector {
fn kind(&self) -> SignalKind {
SignalKind::HsErr
}
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
let dirs = if self.dirs.is_empty() {
vec![ctx.run_dir.clone()]
} else {
self.dirs.clone()
};
let mut newest: Option<PathBuf> = None;
for dir in dirs {
if let Some(path) = newest_file(&dir, ctx.started_at, |name| {
name.starts_with("hs_err_pid") && name.ends_with(".log")
}) {
newest = match newest {
Some(current) => match (modified_at(&current), modified_at(&path)) {
(Some(a), Some(b)) if b <= a => Some(current),
_ => Some(path),
},
None => Some(path),
};
}
}
let Some(path) = newest else {
return Ok(CollectedSources::new());
};
let report = hs_err::parse_file(&path).map_err(|error| CollectError {
kind: SignalKind::HsErr,
path: Some(path.clone()),
message: error.to_string(),
})?;
let mut sources = CollectedSources::new();
sources.push(&path);
out.sources.push(path);
out.hs_err = Some(report);
Ok(sources)
}
}
/// 磁盘类:`logs/latest.log` 尾巴(内存缓冲不够长时的兜底)。
#[derive(Debug, Clone)]
pub struct LatestLogCollector {
path: Option<PathBuf>,
max_lines: usize,
}
impl Default for LatestLogCollector {
fn default() -> Self {
Self::new()
}
}
impl LatestLogCollector {
/// 默认读 `<run_dir>/logs/latest.log` 的最后 5000 行。
#[must_use]
pub fn new() -> Self {
Self {
path: None,
max_lines: 5000,
}
}
/// 保留行数上限。
#[must_use]
pub fn with_max_lines(mut self, max_lines: usize) -> Self {
self.max_lines = max_lines.max(1);
self
}
/// 指定文件。
#[must_use]
pub fn with_path(path: impl Into<PathBuf>) -> Self {
Self {
path: Some(path.into()),
max_lines: 5000,
}
}
}
impl SignalCollector for LatestLogCollector {
fn kind(&self) -> SignalKind {
SignalKind::Log
}
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
let path = self
.path
.clone()
.unwrap_or_else(|| ctx.run_dir.join("logs").join("latest.log"));
if !path.is_file() {
return Ok(CollectedSources::new());
}
let text = read_lossy(SignalKind::Log, &path)?;
let lines = text.lines().collect::<Vec<_>>();
let start = lines.len().saturating_sub(self.max_lines);
out.log
.extend(SignalOrigin::LatestLog, lines[start..].iter().copied());
let mut sources = CollectedSources::new();
sources.push(&path);
out.sources.push(path);
Ok(sources)
}
}
fn newest_file(
dir: &Path,
since: Option<SystemTime>,
matches: impl Fn(&str) -> bool,
) -> Option<PathBuf> {
let entries = fs::read_dir(dir).ok()?;
entries
.flatten()
.filter_map(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
if !matches(&name) {
return None;
}
let path = entry.path();
let modified = modified_at(&path)?;
if since.is_some_and(|since| modified < since) {
return None;
}
Some((modified, path))
})
.max_by(|a, b| a.0.cmp(&b.0))
.map(|(_, path)| path)
}
fn modified_at(path: &Path) -> Option<SystemTime> {
fs::metadata(path).ok()?.modified().ok()
}
fn read_lossy(kind: SignalKind, path: &Path) -> Result<String, CollectError> {
let bytes = fs::read(path).map_err(|error| CollectError::io(kind, path, &error))?;
Ok(String::from_utf8_lossy(&bytes).into_owned())
}