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

+20 -2
View File
@@ -26,6 +26,12 @@
//! ⚠️ 外部命令的退出码**不能**用来判断“进程是否已被结束”:温和阶段成功关掉进程后,
//! 再发 `taskkill /F` 会得到 128(“没有这个运行实例”)。所以 [`crate::launch::process`]
//! 的实际停止流程以“进程是否真的消失”为准(`try_wait` 轮询),本模块只提供发送动作。
//!
//! # 送达即登记(D10)
//!
//! 每次信号**真的送达**都记进 [`crate::launch::kill_registry`],退出分析据此把结局判成
//! 「被终止」而不是「应用错误」。登记放在这一层,是因为所有结束路径都经过这里
//! (`RunningProcess::stop` / GUI 的 `host::launch::stop` / 看门狗兜底),调用点不必各记一遍。
use std::io;
@@ -34,13 +40,25 @@ use crate::platform::child::quiet;
/// 温和关闭(Windows 无 `/F`;POSIX `SIGTERM`)。
pub fn terminate(pid: u32) -> io::Result<()> {
check_pid(pid)?;
imp::signal(pid, false)
signal_and_note(pid, false)
}
/// 强制关闭(Windows `/F`;POSIX `SIGKILL`),并尽量覆盖整棵进程树。
pub fn force_kill(pid: u32) -> io::Result<()> {
check_pid(pid)?;
imp::signal(pid, true)
signal_and_note(pid, true)
}
/// 发信号,**送达后**登记「这个 pid 是我们结束的」(D10)。
///
/// 只在送达时登记:`taskkill` 对已退出的进程返回非 0、POSIX 的 `kill` 会报「没有送达」,
/// 那两种情况说明它自己先走了——不能把自然退出洗成「被结束」。
fn signal_and_note(pid: u32, force: bool) -> io::Result<()> {
let result = imp::signal(pid, force);
if result.is_ok() {
crate::launch::kill_registry::note(pid);
}
result
}
/// 三段式:温和 → 宽限 → 强制。返回**强制阶段**的结果(温和阶段失败不致命)。