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

+47
View File
@@ -68,6 +68,12 @@ pub const CRASH_KEYWORDS: &[&str] = &[
/// 静默失败的存活阈值(毫秒):<30s 视为“没真正跑起来”。
pub const SHORT_LIVED_MS: u64 = 30_000;
/// 「被外部结束」在 Windows 上的退出码。
///
/// 任务管理器「结束任务」与 `taskkill /F` 都走 `TerminateProcess(h, 1)`,进程侧只能看到 1;
/// 真正的应用崩溃在 Windows 上给的是 NTSTATUS 异常码(`0xC0000005` 等),不会落在 1。
pub const WINDOWS_TERMINATED_CODE: i32 = 1;
/// 退出分析的输入。
#[derive(Debug, Clone, Copy)]
pub struct ExitSignals<'a> {
@@ -139,6 +145,11 @@ pub struct ExitAnalysis {
pub silent_failure: Option<SilentFailure>,
/// Windows NTSTATUS 解释(例 `0xC0000005 ACCESS_VIOLATION`)。
pub windows_status: Option<String>,
/// 是否**疑似被外部结束**(任务管理器 / 强杀),而不是应用自己崩了(D10)。
///
/// 判据见 [`looks_externally_terminated`]:Windows 上退出码 1 且零错误输出。
/// 与「我们杀的」区分开:那条路有 [`crate::launch::kill_registry`] 作据,是**确知**的。
pub terminated_externally: bool,
/// 是否应该进入诊断流水线(M15)。
pub needs_diagnosis: bool,
}
@@ -156,6 +167,9 @@ impl ExitAnalysis {
if let Some(status) = &self.windows_status {
text.push_str(&format!(",{status}"));
}
if self.terminated_externally {
text.push_str(",零错误输出:疑似被外部结束(任务管理器 / 强杀)");
}
if !self.crash_keywords.is_empty() {
text.push_str(&format!(",崩溃关键字 {}", self.crash_keywords.len()));
}
@@ -186,6 +200,9 @@ pub fn analyze(signals: &ExitSignals<'_>) -> ExitAnalysis {
ExitType::SigKill
} else if signals.is_posix && signals.code == 143 {
ExitType::SigTerm
} else if looks_externally_terminated(signals, error_lines, &crash_keywords, &jvm_keywords) {
// D10:任务管理器 / `taskkill /F` 给的就是「1 + 零错误输出」,不是应用错误。
ExitType::Killed
} else if signals.code != 0 || !crash_keywords.is_empty() {
ExitType::ApplicationError
} else {
@@ -215,12 +232,42 @@ pub fn analyze(signals: &ExitSignals<'_>) -> ExitAnalysis {
jvm_keywords,
silent_failure,
windows_status: windows_status_name(signals.code),
terminated_externally: exit_type == ExitType::Killed
&& !signals.killed
&& !signals.interrupted,
needs_diagnosis: exit_type.is_abnormal()
|| silent_failure.is_some()
|| !crash_keywords.is_empty(),
}
}
/// 外部结束的判据(D10)。
///
/// 三条同时成立才算:
///
/// 1. **不是我们杀的**(`signals.killed`)——那条路有 [`crate::launch::kill_registry`] 作据;
/// 2. Windows 上退出码 [`WINDOWS_TERMINATED_CODE`](`TerminateProcess(h, 1)` 的痕迹);
/// 3. **零错误输出**:没有错误行、没有崩溃关键字、没有 JVM 失败关键字。
///
/// 「1 + 一句话的报错都没有」不属于应用崩溃:应用崩在 Windows 上给 NTSTATUS 异常码,
/// JVM 起不来会打 `Could not create the Java Virtual Machine.`,`System.exit(1)` 之前
/// 一般也有日志。剩下最可能的解释就是「被外面按了结束」——挂死之后任务管理器强杀正是这种形态。
#[must_use]
pub fn looks_externally_terminated(
signals: &ExitSignals<'_>,
error_lines: usize,
crash_keywords: &[String],
jvm_keywords: &[String],
) -> bool {
!signals.killed
&& !signals.interrupted
&& !signals.is_posix
&& signals.code == WINDOWS_TERMINATED_CODE
&& error_lines == 0
&& crash_keywords.is_empty()
&& jvm_keywords.is_empty()
}
/// Windows NTSTATUS → 解释。
pub fn windows_status_name(code: i32) -> Option<String> {
let (hex, name) = match code {