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

+396
View File
@@ -0,0 +1,396 @@
//! 采集到的原子信号:日志环形缓冲 + 退出信号。
//!
//! 五类信号里的「退出码」和「日志环形缓冲」两种(analyse/02 §7.1 信号 A/B)。
//! 环境快照(信号 E)与 mod 清单在 [`crate::diagnose::collectors`] 里,因为它带采集动作。
//!
//! 环形缓冲相对 HMCL `Log.java`(固定 2000 行、一律淘汰)的升级:
//! 命中 `Exception|Error|Caused by|OutOfMemory` 的行**永久保留**,
//! 解决「崩在启动早期、根因被后续输出挤出缓冲区」(analyse/02 §7.10-3)。
use std::collections::VecDeque;
use std::fmt;
/// 一条日志文本的来源(诊断输出的 `evidence[]` 前缀也是它)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SignalOrigin {
/// 游戏进程 stdout。
Stdout,
/// 游戏进程 stderr。
Stderr,
/// `logs/latest.log`(内存缓冲不够长时的兜底)。
LatestLog,
/// `crash-reports/crash-*.txt`。
CrashReport,
/// `hs_err_pid*.log` 的摘要文本。
HsErr,
/// 启动器自身日志 / 结构化事件(`cmd_built`、`assembly_failed` 等)。
Launcher,
}
impl SignalOrigin {
/// 稳定字符串(evidence 前缀 / 序列化用)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Stdout => "stdout",
Self::Stderr => "stderr",
Self::LatestLog => "latest_log",
Self::CrashReport => "crash_report",
Self::HsErr => "hs_err",
Self::Launcher => "launcher",
}
}
/// `launch` 侧 `LineSink` 的 `stream` 字段映射(`stdout` / `stderr`)。
#[must_use]
pub fn from_stream_name(stream: &str) -> Self {
match stream.trim().to_ascii_lowercase().as_str() {
"stderr" => Self::Stderr,
_ => Self::Stdout,
}
}
/// 取证优先级:同一规则在多个来源命中时保留 rank 最小者(崩溃报告 > hs_err > 日志)。
#[must_use]
pub const fn evidence_rank(self) -> u8 {
match self {
Self::CrashReport => 0,
Self::HsErr => 1,
Self::Launcher => 2,
Self::LatestLog => 3,
Self::Stdout => 4,
Self::Stderr => 5,
}
}
}
impl fmt::Display for SignalOrigin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 环形缓冲里的一行。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogLine {
/// 来源。
pub origin: SignalOrigin,
/// 行文本(不含行尾换行)。
pub text: String,
}
impl LogLine {
/// 构造一行。
#[must_use]
pub fn new(origin: SignalOrigin, text: impl Into<String>) -> Self {
Self {
origin,
text: text.into(),
}
}
}
/// 是否「关键行」:不随环形淘汰(HMCL 局限 3 的正面修正)。
#[must_use]
pub fn is_key_line(text: &str) -> bool {
const MARKERS: [&str; 4] = ["Exception", "Error", "Caused by", "OutOfMemory"];
MARKERS.iter().any(|marker| text.contains(marker))
}
/// 日志环形缓冲:最近 N 行 + 被判为关键行的历史永久保留。
#[derive(Debug, Clone)]
pub struct LogBuffer {
capacity: usize,
key_capacity: usize,
recent: VecDeque<LogLine>,
/// 已从环形区淘汰、但因为是关键行而留下来的行(按时间顺序)。
kept: VecDeque<LogLine>,
pushed: usize,
dropped: usize,
}
impl Default for LogBuffer {
fn default() -> Self {
Self::new(2000)
}
}
impl LogBuffer {
/// 默认容量 2000 行(与 HMCL 的 `Log` 一致)。
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
key_capacity: capacity.max(1),
recent: VecDeque::new(),
kept: VecDeque::new(),
pushed: 0,
dropped: 0,
}
}
/// 关键行保留上限(默认 = 环形容量)。
#[must_use]
pub fn with_key_capacity(mut self, key_capacity: usize) -> Self {
self.key_capacity = key_capacity.max(1);
self
}
/// 记一行(`launch` 的 stdout/stderr 事件逐行喂进来)。
pub fn push(&mut self, origin: SignalOrigin, line: &str) {
let text = line.trim_end_matches(['\r', '\n']).to_owned();
self.pushed += 1;
if self.recent.len() >= self.capacity {
if let Some(evicted) = self.recent.pop_front() {
self.dropped += 1;
if is_key_line(&evicted.text) {
if self.kept.len() >= self.key_capacity {
self.kept.pop_front();
}
self.kept.push_back(evicted);
}
}
}
self.recent.push_back(LogLine::new(origin, text));
}
/// 一次性喂多行(导入日志、`latest.log` 兜底)。
pub fn extend<'a, I>(&mut self, origin: SignalOrigin, lines: I)
where
I: IntoIterator<Item = &'a str>,
{
for line in lines {
self.push(origin, line);
}
}
/// 按时间顺序返回全部保留行(关键行历史在前,最近 N 行在后)。
pub fn lines(&self) -> impl Iterator<Item = &LogLine> {
self.kept.iter().chain(self.recent.iter())
}
/// 拼成可供规则匹配的文本(`\n` 连接)。
#[must_use]
pub fn text(&self) -> String {
let mut out = String::with_capacity(self.byte_len());
for line in self.lines() {
out.push_str(&line.text);
out.push('\n');
}
out
}
/// 当前保留行数。
#[must_use]
pub fn len(&self) -> usize {
self.kept.len() + self.recent.len()
}
/// 是否一行都没有。
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// 一共推入多少行。
#[must_use]
pub const fn pushed(&self) -> usize {
self.pushed
}
/// 被淘汰多少行(不含被永久保留的关键行)。
#[must_use]
pub const fn dropped(&self) -> usize {
self.dropped
}
/// 因关键行规则而保留下来的历史行数。
#[must_use]
pub fn kept_key_lines(&self) -> usize {
self.kept.len()
}
fn byte_len(&self) -> usize {
self.lines().map(|line| line.text.len() + 1).sum()
}
}
/// 退出类型。字符串值与 `launch-接口约定.md` §3 `exit.exit_type` 完全一致。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum ExitType {
/// 正常退出(但可能是静默失败,见 [`ExitSignal::silent_failure`])。
#[default]
Normal,
/// 应用层崩溃(有崩溃关键字 / 非 0 退出码)。
ApplicationError,
/// JVM 没起来(`-Xmx` 过大、参数非法、JVM 致命错误)。
JvmError,
/// 被 SIGKILL(Linux 137 / 系统 OOM killer)。
Sigkill,
/// 被 SIGTERM(143)。
Sigterm,
/// 被启动器主动停止(用户取消 / watchdog)。
Killed,
/// 等待被中断(会话被丢弃)。
Interrupted,
}
impl ExitType {
/// 稳定字符串(与 launch 约定同值)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::ApplicationError => "application_error",
Self::JvmError => "jvm_error",
Self::Sigkill => "sigkill",
Self::Sigterm => "sigterm",
Self::Killed => "killed",
Self::Interrupted => "interrupted",
}
}
/// 从 launch 约定字符串反解(未知值回落 `ApplicationError`,绝不 panic)。
#[must_use]
pub fn from_name(name: &str) -> Self {
match name.trim().to_ascii_lowercase().as_str() {
"normal" => Self::Normal,
"jvm_error" => Self::JvmError,
"sigkill" => Self::Sigkill,
"sigterm" => Self::Sigterm,
"killed" => Self::Killed,
"interrupted" => Self::Interrupted,
_ => Self::ApplicationError,
}
}
/// 是否异常结束(非 `Normal`)。
#[must_use]
pub const fn is_abnormal(self) -> bool {
!matches!(self, Self::Normal)
}
}
impl fmt::Display for ExitType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// JVM 没起来的三个签名(analyse/02 §7.2,`JVM_ERROR` 判定依据)。
pub const JVM_ERROR_KEYWORDS: [&str; 3] = [
"Could not create the Java Virtual Machine",
"Error occurred during initialization of VM",
"A fatal exception has occurred. Program will exit.",
];
/// 应用层崩溃关键字(HMCL `ExitWaiter` 的 5 条 + 我们能拿到的补充)。
pub const CRASH_KEYWORDS: [&str; 5] = [
"Game crashed! Crash report saved to",
"Exception in thread",
"Unhandled exception",
"Unable to launch",
"An exception was thrown, the game will display an error screen and halt",
];
/// Windows 常见退出码(`0xC0000005` 等按 `i32` 表示,analyse/02 §8.4-2)。
pub const EXIT_ACCESS_VIOLATION: i32 = -1_073_741_819;
/// `0xC0000409` STACK_BUFFER_OVERRUN。
pub const EXIT_STACK_BUFFER_OVERRUN: i32 = -1_073_740_791;
/// `0xC0000135` DLL_NOT_FOUND。
pub const EXIT_DLL_NOT_FOUND: i32 = -1_073_741_515;
/// POSIX SIGKILL(128 + 9)。
pub const EXIT_SIGKILL: i32 = 137;
/// POSIX SIGTERM(128 + 15)。
pub const EXIT_SIGTERM: i32 = 143;
/// 进程退出信号(`launch` 的 `exit` 事件映射过来;离线诊断时由 [`ExitSignal::classify`] 反推)。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExitSignal {
/// 退出码(被信号杀死时为负或 128+N)。
pub code: Option<i32>,
/// 退出类型。
pub exit_type: ExitType,
/// 进程存活毫秒数。
pub uptime_ms: Option<u64>,
/// stderr/错误行条数。
pub error_lines: u32,
/// 退出码 0 但疑似静默失败(存活 <30s 或 stderr 有错误特征)。
pub silent_failure: bool,
/// Windows 状态码原始串(`0xc0000005`)。
pub windows_status: Option<String>,
/// 命中的崩溃关键字。
pub crash_keywords: Vec<String>,
}
impl ExitSignal {
/// 从原始退出事实推断类型(离线诊断 / 导入日志用;launch 侧有权威版本)。
#[must_use]
pub fn classify(
code: Option<i32>,
uptime_ms: Option<u64>,
error_lines: u32,
error_text: &str,
) -> Self {
let has = |keywords: &[&str]| keywords.iter().any(|kw| error_text.contains(kw));
let mut crash_keywords = Vec::new();
for keyword in CRASH_KEYWORDS {
if error_text.contains(keyword) {
crash_keywords.push(keyword.to_owned());
}
}
let exit_type = match code {
Some(EXIT_SIGKILL) => ExitType::Sigkill,
Some(EXIT_SIGTERM) => ExitType::Sigterm,
// 与 HMCL `ExitType` 一致:只有「非 0 退出码 + JVM 关键字」才算 JVM_ERROR。
// exit 0 却打出这些关键字的场景归到靜默失败(silent_failure)而不是 JVM 错误。
Some(0) => ExitType::Normal,
Some(_) if has(&JVM_ERROR_KEYWORDS) => ExitType::JvmError,
Some(_) => ExitType::ApplicationError,
None => ExitType::ApplicationError,
};
let short_lived = uptime_ms.is_some_and(|ms| ms < 30_000);
let silent_failure = exit_type == ExitType::Normal
&& (short_lived || error_lines > 0 || has(&CRASH_KEYWORDS));
Self {
code,
exit_type,
uptime_ms,
error_lines,
silent_failure,
windows_status: code.and_then(hex_status),
crash_keywords,
}
}
/// Windows 状态码的十六进制串(如 `0xc0000005`)。
#[must_use]
pub fn status_hex(&self) -> Option<String> {
self.windows_status
.clone()
.or_else(|| self.code.and_then(hex_status))
}
/// 退出码是否是 Windows 访问违例(`0xC0000005`)。
#[must_use]
pub fn is_access_violation(&self) -> bool {
self.code == Some(EXIT_ACCESS_VIOLATION)
}
/// 一句话摘要(写进诊断报告 / 证据行)。
#[must_use]
pub fn summary(&self) -> String {
let code = self
.code
.map_or_else(|| "未知".to_owned(), |code| code.to_string());
let uptime = self
.uptime_ms
.map_or_else(String::new, |ms| format!(",存活 {} ms", ms));
format!("退出码 {code}({}){uptime}", self.exit_type)
}
}
fn hex_status(code: i32) -> Option<String> {
// 只有 Windows 的 NTSTATUS 形态(负数高位为 1)才有意义。
(code < 0).then(|| format!("0x{:08x}", code as u32))
}