Files
Unlucky-Launcher/crates/core/src/launch/error.rs
T
AskaEth 75aa5ee82c fix(cli): 启动用的 Java 钉死 D4 校验过的那个 + M214 复审 nit1/nit2/nit3
- override_path 五来源统一钉住 D4 选中的运行时:以前只有 managed 钉路径,
  JAVA_HOME/PATH/system 三种会被 snapshot::prepare 的 selection() 按环境再挑一遍,
  D4 判过的 Java 根本不是真启动的那个。现在 `plan_java` 只做「选中 → 钉路径」,
  人读行写出「版本(来源 · 具体可执行文件)」,--json 的 java 块并排给出
  binary(校验过的)/ launch_binary(装配落地的)/ source,两者不一致时当场告警。
  实例设置/全局设置里的 javaPath 也并入「谁指定了 Java」,不会被自动挑的结果顶掉。
- nit3:D4 的 Java 不符拦截挪到 identity::resolve 之前(微软轨解析身份会联网刷新
  令牌、还会回写凭据),拦下的启动不再先触发一次刷新;不符提示补上「挑中的是哪个
  java」,文案只加不减。
- nit1:error.rs / launcher.rs 的人读串里残留的 ** 标记去掉,并补两条回归断言。
- nit2:java_launch.rs 里「只提示不阻断」等旧注释对齐 D4 现状。
- launch.rs(399 行零余量):D20 的隔离档位派生与切换检测整体搬到新文件
  launch_isolation.rs,纯挪函数体(逐行 diff 只多 6 处 pub(super)),
  用 #[path] 挂载以免改 commands/mod.rs。

用例:钉路径纯函数单测 3 条 + 端到端 2 条(JAVA_HOME 置空证明非 managed 来源也钉住;
数据根放两个账户证明 D4 拦在身份解析之前)+ 人读串无 ** 的 2 条回归。
两条变异(①退回只钉 Managed ②退回身份解析之后)各自让新用例变红,已实测。
2026-11-09 21:30:55 +08:00

202 lines
6.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! launch 模块统一错误类型。
//!
//! 设计依据:`DevDocs/PlanDoc/06-launch-and-diagnostics.md` §1、`analyse/02` §8.4-3
//! (缺库/缺组件必须结构化报错并显式失败,而不是交给运行时 `NoClassDefFoundError`)。
use std::fmt;
use std::path::PathBuf;
/// launch 相关操作的统一结果类型。
pub type Result<T> = std::result::Result<T, LaunchError>;
/// 缺失的组件(库 / natives / 资产),供 UI 与 agent 直接展示与修复。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingComponent {
/// 逻辑名,例如 `org.lwjgl:lwjgl:3.3.1` 或 `libraries/foo/bar.jar`。
pub name: String,
/// 期望落盘路径(未知时为 `None`)。
pub path: Option<PathBuf>,
}
impl MissingComponent {
/// 只按名字构造(路径未知)。
pub fn named(name: impl Into<String>) -> Self {
Self {
name: name.into(),
path: None,
}
}
/// 带期望路径构造。
pub fn at_path(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self {
name: name.into(),
path: Some(path.into()),
}
}
}
/// 命令行组装阶段的失败原因(全部是“启动前就能确定”的错误)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssemblyError {
/// 清单里没有主类,无法启动(HMCL 在此直接抛异常)。
MissingMainClass,
/// classpath 为空(一个库和主 jar 都没有)。
EmptyClasspath,
/// Java 可执行文件路径为空。
EmptyJavaPath,
/// 组件不完整:启动前显式失败,附结构化清单。
MissingComponents(Vec<MissingComponent>),
/// 清单参数畸形(例如空参数项)。
MalformedArgument {
/// 原始参数项文本。
entry: String,
/// 原因说明。
reason: &'static str,
},
/// natives 解压失败(jar 缺失/损坏、目录不可写)。
///
/// 归到「启动前失败」而不是运行时错误:native 库没落地时游戏只会给出
/// `UnsatisfiedLinkError`,那时候已经离「到底哪个 jar 坏了」太远了。
NativesExtraction {
/// 人读原因(含失败路径)。
message: String,
},
}
impl fmt::Display for AssemblyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingMainClass => f.write_str("版本清单缺少主类(mainClass)"),
Self::EmptyClasspath => f.write_str("classpath 为空:没有任何库或主 jar"),
Self::EmptyJavaPath => f.write_str("Java 可执行文件路径为空"),
Self::MissingComponents(items) => {
write!(f, "组件缺失 {} 项", items.len())?;
for item in items.iter().take(8) {
write!(f, "; {}", item.name)?;
}
Ok(())
}
Self::MalformedArgument { entry, reason } => {
write!(f, "参数畸形({reason}):{entry}")
}
Self::NativesExtraction { message } => {
write!(f, "natives 解压失败:{message}")
}
}
}
}
impl std::error::Error for AssemblyError {}
/// launch 模块的统一错误。
#[derive(Debug)]
pub enum LaunchError {
/// 命令行组装失败(纯计算,可单测)。
Assembly(AssemblyError),
/// 文件系统操作失败。
Io {
/// 操作名(`create_session` / `read_argfile` 等)。
op: &'static str,
/// 相关路径。
path: PathBuf,
/// 底层错误。
source: std::io::Error,
},
/// 进程派生失败(Java 不存在、权限不足等)。
Spawn {
/// 被执行的程序。
program: PathBuf,
/// 底层错误。
source: std::io::Error,
},
/// 会话事件流写入/解析失败。
Session {
/// 会话文件路径。
path: PathBuf,
/// 原因说明。
reason: String,
},
/// 进程树 kill 失败。
Kill {
/// 目标进程号。
pid: u32,
/// 底层错误。
source: std::io::Error,
},
/// 看门狗到点**停止等待**(D6):进程仍在运行,我们只是不再等它了。
Watchdog {
/// 目标进程号(没有被结束)。
pid: u32,
/// 判据 id:`startup_timeout` / `shutdown_timeout`。
reason: &'static str,
/// 已经等了多久(毫秒)。
waited_ms: u64,
/// 判据上限(毫秒)。
limit_ms: u64,
/// 会话事件流(现场)。
log_path: PathBuf,
},
}
impl fmt::Display for LaunchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Assembly(err) => write!(f, "命令行组装失败:{err}"),
Self::Io { op, path, source } => {
write!(f, "{op} 失败({}):{source}", path.display())
}
Self::Spawn { program, source } => {
write!(f, "启动进程失败({}):{source}", program.display())
}
Self::Session { path, reason } => {
write!(f, "会话事件流错误({}):{reason}", path.display())
}
Self::Kill { pid, source } => {
write!(f, "结束进程树失败(pid {pid}):{source}")
}
Self::Watchdog {
pid,
reason,
waited_ms,
limit_ms,
log_path,
} => {
let what = if *reason == "shutdown_timeout" {
"游戏已开始关闭但一直没退出"
} else {
"启动后一直没有任何日志、也没出现游戏窗口"
};
write!(
f,
"{what}:等了 {:.0}s(上限 {:.0}s)就停止等待了。进程 {pid} 仍在运行、没有被结束\
(可能只是加载得慢,也可能真卡住了);事件流见 {}。放宽上限:设环境变量 \
ULL_LAUNCH_STARTUP_TIMEOUT_SECS / ULL_LAUNCH_SHUTDOWN_GRACE_SECS(秒,0 = 不限)",
*waited_ms as f64 / 1000.0,
*limit_ms as f64 / 1000.0,
log_path.display()
)
}
}
}
}
impl std::error::Error for LaunchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Assembly(err) => Some(err),
Self::Io { source, .. } | Self::Spawn { source, .. } | Self::Kill { source, .. } => {
Some(source)
}
Self::Session { .. } => None,
Self::Watchdog { .. } => None,
}
}
}
impl From<AssemblyError> for LaunchError {
fn from(err: AssemblyError) -> Self {
Self::Assembly(err)
}
}