fix(core): 老版本 Java 按版本距离选 + 启动看门狗有界等待 + 结束归属(M214 D4/D6/D10)

D4:`pick` 的排序键从「来源序」改成「版本距离 → 来源序」,全不满足时选最接近要求的那个
(托管 25 与 JAVA_HOME/扫盘 17 之间选 17,而不是按 rank 选托管)。新增 `select::decide`:
不符且**没有人显式指定 Java** 时 `ull launch` 直接报 `java_mismatch` 不 spawn(`--java` /
实例 `javaPath` 是豁免,只告警)——1.12.2 配 Java 17/25 必然起不来,警告没有信息量。
`JavaMismatch` + `check` 拆到 `mismatch.rs` 并加 `advice()`(mapping.rs 408→282 行)。

D6:`LaunchSession::finish` 的等进程改成**有界**。判据在 `startup_watch.rs`(纯逻辑):
①窗口从未出现且**连续 N 秒没有任何日志**(还在打日志就说明还在加载,慢加载不被判死);
②日志出现关闭标记后 N 秒仍未退出(实机「关窗后 java.exe 仍存活」的形状)。
到点只**停止等待、绝不杀进程**:写 `session_end(still_running)` + 索引 + `launcher.log`,
返回 `LaunchError::Watchdog`,CLI 翻成 `launch_timeout`。两个时长可配
(默认 300s/60s;`ULL_LAUNCH_STARTUP_TIMEOUT_SECS` / `ULL_LAUNCH_SHUTDOWN_GRACE_SECS`,
生效值每次启动写进 launcher.log 与人读输出)。

D10:把「被终止」与「应用错误」分开。①我们自己杀的:`tree_kill` **送达即登记**
`kill_registry`(pid 登记簿,取走语义、上限 64),覆盖 GUI 从别的线程 `kill_tree` 的路径,
`finish` 据此判 `killed`;②外部强杀:Windows 上「退出码 1 + 零错误输出 + 无崩溃/JVM 关键字」
判 `killed` 并标 `terminated_externally`(任务管理器与 `taskkill /F` 都是
`TerminateProcess(h, 1)`,应用真崩给的是 NTSTATUS 异常码)。

顺带:`SessionHandle` 拆 `session_handle.rs`(launch_session.rs 422→343 行)、
`NativesReport::to_json` 落 core(cli/launch.rs 腾出空间,仍 399 行)。
用例:select 距离/取舍、startup_watch 状态机、watchdog_cases(4 条真进程)、
exit_cases(外部终止与「我们杀的」区分)、pipeline 跨线程 kill。
This commit is contained in:
AskaEth committed 2026-11-09 17:07:22 +08:00
1 parent 5959a27add
commit ff78fe2cae
23 files changed
+1708 -250

No files matched your search

+9 -103
View File
@@ -1,4 +1,6 @@
//! MC 版本 ↔ Java 版本映射(平移 HMCL 约束体系)+ 不符检查。
//! MC 版本 ↔ Java 版本映射(平移 HMCL 约束体系);不符判定与文案见
//! [`crate::game::java::mismatch`](其中的 `check` / `JavaMismatch` 仍从本模块转出,
//! 调用方的 `mapping::check` 写法不变)。
//!
//! 依据 `DevDocs/analyse/02-launch-pipeline.md` §5.4(HMCL `GameJavaVersion.getMinimumJavaVersion`)
//! 与 §3.1 的强制约束清单(`JavaVersionConstraint` 的 `isMandatory` 项)。两条权威叠加:
@@ -25,6 +27,8 @@ use super::super::inherit::resolve;
use super::super::layout::GameLayout;
use super::super::manifest::{JavaVersion, VersionJson};
pub use super::mismatch::{check, JavaMismatch};
/// 要求来源(`--json` 的 `requirement_source`)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequirementSource {
@@ -90,66 +94,6 @@ impl JavaRequirement {
}
}
/// 不符的两种形态(D13:只提示,不阻断)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaMismatch {
/// Java 太低。
TooOld {
/// 需要的最低主版本。
required: u32,
/// 实际主版本。
actual: u32,
},
/// Java 太高(老版本只能用 8)。
TooNew {
/// 允许的最高主版本。
maximum: u32,
/// 实际主版本。
actual: u32,
},
}
impl JavaMismatch {
/// 稳定字符串 id(`--json` 的 `mismatch.code`)。
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::TooOld { .. } => "java_too_old",
Self::TooNew { .. } => "java_too_new",
}
}
/// 人读说明(GUI 弹窗 P1 后直接复用这句)。
#[must_use]
pub fn message(self) -> String {
match self {
Self::TooOld { required, actual } => format!(
"该版本需要 Java {required} 及以上,当前用的是 Java {actual}(大概率起不来)"
),
Self::TooNew { maximum, actual } => format!(
"该版本只能用 Java {maximum} 及以下,当前用的是 Java {actual}(老加载器吃不了新 class 文件)"
),
}
}
}
/// 判需求与实际是否相符(`None` = 相符)。
#[must_use]
pub fn check(actual: u32, required: &JavaRequirement) -> Option<JavaMismatch> {
if actual < required.minimum {
return Some(JavaMismatch::TooOld {
required: required.minimum,
actual,
});
}
if let Some(maximum) = required.maximum {
if actual > maximum {
return Some(JavaMismatch::TooNew { maximum, actual });
}
}
None
}
/// 静态表:MC 版本 → Java 要求。
///
/// 无法解析的版本(周快照 `20w14a`、魔改 id)只给下限 `8`、不设上限——
@@ -281,7 +225,7 @@ fn at_least(parts: &[u32], floor: &[u32]) -> bool {
#[cfg(test)]
mod tests {
use super::{check, recommended_major, requirement_for, JavaMismatch, JavaRequirement};
use super::{recommended_major, requirement_for, JavaRequirement};
#[test]
fn table_covers_the_hmcl_boundaries() {
@@ -330,47 +274,9 @@ mod tests {
}
#[test]
fn check_reports_both_directions() {
let modern = JavaRequirement::table(21, None);
assert_eq!(check(21, &modern), None);
assert_eq!(check(25, &modern), None);
assert_eq!(
check(17, &modern),
Some(JavaMismatch::TooOld {
required: 21,
actual: 17
})
);
let legacy = JavaRequirement::table(8, Some(8));
assert_eq!(check(8, &legacy), None);
assert_eq!(
check(21, &legacy),
Some(JavaMismatch::TooNew {
maximum: 8,
actual: 21
})
);
assert_eq!(legacy.describe(), "Java 8");
assert_eq!(modern.describe(), "Java 21+");
fn describe_spells_out_the_interval() {
assert_eq!(requirement_for("1.12.2").describe(), "Java 8");
assert_eq!(requirement_for("1.20.5").describe(), "Java 21+");
assert_eq!(JavaRequirement::table(8, Some(16)).describe(), "Java 8–16");
}
#[test]
fn mismatch_messages_name_both_sides() {
let message = JavaMismatch::TooOld {
required: 21,
actual: 8,
}
.message();
assert!(message.contains("Java 21"));
assert!(message.contains("Java 8"));
assert_eq!(
JavaMismatch::TooNew {
maximum: 8,
actual: 21
}
.code(),
"java_too_new"
);
}
}