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

+264
View File
@@ -0,0 +1,264 @@
//! 退出分析:退出码语义 + `exit 0` 静默失败检测。
//!
//! 平移自 `analyse/02` §6.3/§7.2(`ExitWaiter` 的三源判定),并按 §8.4-2 / §7.10-10 做两处升级:
//!
//! 1. **完整退出码表**:不只认 Linux 的 `137`,Windows 的 NTSTATUS
//! (`0xC0000005` 等)也要能说清是什么;
//! 2. **静默失败单独立标**:退出码 0 但存活 <30s 或 stderr 带错误特征时标记
//! `silent_failure`——HMCL 会判 NORMAL 放过(LaunchWrapper 吞异常后 `exit 0` 是
//! 真实存在的失败形态)。
use crate::launch::pump::LogLine;
/// 退出判定类型。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitType {
/// 正常退出。
Normal,
/// 应用层崩溃(游戏起来了但崩了)。
ApplicationError,
/// JVM 都没起来(`-Xmx` 过大、JVM 参数非法等)。
JvmError,
/// 被 `SIGKILL` 结束(POSIX 137)。
SigKill,
/// 被 `SIGTERM` 结束(POSIX 143)。
SigTerm,
/// 被启动器主动结束(用户点取消 / 看门狗强制)。
Killed,
/// 等待被中断。
Interrupted,
}
impl ExitType {
/// 事件流里的字符串形式(与 M15 `ExitType` 刻意保持一致)。
pub 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",
}
}
/// 是否属于“非正常退出”。
pub fn is_abnormal(self) -> bool {
self != Self::Normal
}
}
/// JVM 未能启动的三条关键字(`analyse/02` §7.2)。
pub const JVM_FAILURE_KEYWORDS: &[&str] = &[
"Could not create the Java Virtual Machine.",
"Error occurred during initialization of VM",
"A fatal exception has occurred. Program will exit.",
];
/// 应用层崩溃的五条关键字(`analyse/02` §7.2,覆盖三种崩法)。
pub const CRASH_KEYWORDS: &[&str] = &[
"Crash report saved to",
"Could not save crash report to",
"This crash report has been saved to:",
"Unable to launch",
"An exception was thrown, the game will display an error screen and halt.",
];
/// 静默失败的存活阈值(毫秒):<30s 视为“没真正跑起来”。
pub const SHORT_LIVED_MS: u64 = 30_000;
/// 退出分析的输入。
#[derive(Debug, Clone, Copy)]
pub struct ExitSignals<'a> {
/// 退出码。
pub code: i32,
/// 进程存活毫秒数。
pub uptime_ms: u64,
/// 环形缓冲里的日志行。
pub lines: &'a [LogLine],
/// 是否启动器主动结束。
pub killed: bool,
/// 等待是否被中断。
pub interrupted: bool,
/// 是否 POSIX(决定 137/143 是否按信号解读)。
pub is_posix: bool,
}
impl<'a> ExitSignals<'a> {
/// 构造。
pub fn new(code: i32, uptime_ms: u64, lines: &'a [LogLine]) -> Self {
Self {
code,
uptime_ms,
lines,
killed: false,
interrupted: false,
is_posix: cfg!(unix),
}
}
}
/// 静默失败标记。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SilentFailure {
/// 存活时间过短。
pub short_lived: bool,
/// stderr/日志里有错误特征。
pub error_output: bool,
}
impl SilentFailure {
/// 人读说明。
pub fn describe(&self) -> String {
match (self.short_lived, self.error_output) {
(true, true) => "退出码 0,但存活不足 30 秒且输出含错误特征".to_string(),
(true, false) => "退出码 0,但存活不足 30 秒".to_string(),
(false, true) => "退出码 0,但输出含错误特征".to_string(),
(false, false) => "退出码 0".to_string(),
}
}
}
/// 退出分析结果(`exit` 事件的 payload 来源)。
#[derive(Debug, Clone)]
pub struct ExitAnalysis {
/// 退出码。
pub code: i32,
/// 判定类型。
pub exit_type: ExitType,
/// 存活毫秒数。
pub uptime_ms: u64,
/// 错误级日志行数。
pub error_lines: u32,
/// 命中的崩溃关键字。
pub crash_keywords: Vec<String>,
/// 命中的 JVM 失败关键字。
pub jvm_keywords: Vec<String>,
/// 静默失败标记(仅 `Normal` 时可能非空)。
pub silent_failure: Option<SilentFailure>,
/// Windows NTSTATUS 解释(例 `0xC0000005 ACCESS_VIOLATION`)。
pub windows_status: Option<String>,
/// 是否应该进入诊断流水线(M15)。
pub needs_diagnosis: bool,
}
impl ExitAnalysis {
/// 一句话摘要(UI/agent 可直接展示)。
pub fn summary(&self) -> String {
let mut text = format!(
"退出码 {}({}),存活 {:.1}s,错误行 {}",
self.code,
self.exit_type.as_str(),
self.uptime_ms as f64 / 1000.0,
self.error_lines
);
if let Some(status) = &self.windows_status {
text.push_str(&format!(",{status}"));
}
if !self.crash_keywords.is_empty() {
text.push_str(&format!(",崩溃关键字 {}", self.crash_keywords.len()));
}
if let Some(silent) = &self.silent_failure {
text.push_str(&format!(",{}", silent.describe()));
}
text
}
}
/// 做退出分析。
pub fn analyze(signals: &ExitSignals<'_>) -> ExitAnalysis {
let error_lines = signals.lines.iter().filter(|line| line.is_error()).count();
let crash_keywords = matched_keywords(signals.lines, CRASH_KEYWORDS);
let jvm_keywords = matched_keywords(signals.lines, JVM_FAILURE_KEYWORDS);
let error_output = signals
.lines
.iter()
.any(|line| line.is_error() && looks_like_failure(&line.text));
let exit_type = if signals.interrupted {
ExitType::Interrupted
} else if signals.killed {
ExitType::Killed
} else if signals.code != 0 && !jvm_keywords.is_empty() {
ExitType::JvmError
} else if signals.is_posix && signals.code == 137 {
ExitType::SigKill
} else if signals.is_posix && signals.code == 143 {
ExitType::SigTerm
} else if signals.code != 0 || !crash_keywords.is_empty() {
ExitType::ApplicationError
} else {
ExitType::Normal
};
let silent_failure = if exit_type == ExitType::Normal {
let short_lived = signals.uptime_ms < SHORT_LIVED_MS;
if short_lived || error_output {
Some(SilentFailure {
short_lived,
error_output,
})
} else {
None
}
} else {
None
};
ExitAnalysis {
code: signals.code,
exit_type,
uptime_ms: signals.uptime_ms,
error_lines: u32::try_from(error_lines).unwrap_or(u32::MAX),
crash_keywords: crash_keywords.clone(),
jvm_keywords,
silent_failure,
windows_status: windows_status_name(signals.code),
needs_diagnosis: exit_type.is_abnormal()
|| silent_failure.is_some()
|| !crash_keywords.is_empty(),
}
}
/// Windows NTSTATUS → 解释。
pub fn windows_status_name(code: i32) -> Option<String> {
let (hex, name) = match code {
-1_073_741_819 => ("0xC0000005", "ACCESS_VIOLATION"),
-1_073_740_791 => ("0xC0000409", "STACK_BUFFER_OVERRUN"),
-1_073_741_515 => ("0xC0000135", "DLL_NOT_FOUND"),
-1_073_741_571 => ("0xC00000FD", "STACK_OVERFLOW"),
-1_073_741_510 => ("0xC000013A", "CONTROL_C_EXIT"),
_ => return None,
};
Some(format!("{hex} {name}"))
}
/// 退出码的人工提示(附录 C 速查表的裁剪版)。
pub fn exit_code_hint(code: i32, is_posix: bool) -> Option<&'static str> {
match code {
0 => Some("正常退出"),
1 => Some("通用失败(含 JVM 启动失败)"),
137 if is_posix => Some("SIGKILL:被强杀"),
143 if is_posix => Some("SIGTERM:被请求退出"),
_ => None,
}
}
fn matched_keywords(lines: &[LogLine], keywords: &[&str]) -> Vec<String> {
keywords
.iter()
.filter(|keyword| lines.iter().any(|line| line.text.contains(**keyword)))
.map(|keyword| (*keyword).to_string())
.collect()
}
/// “像失败的输出”:显式异常/错误特征(不是任何 stderr 输出都算)。
fn looks_like_failure(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
lower.contains("exception")
|| lower.contains("caused by")
|| lower.contains("fatal")
|| lower.contains("error:")
|| lower.contains("failed")
}