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

+31 -6
View File
@@ -4,32 +4,34 @@
//! 也避免单文件超过 400 行的硬约束。
use std::sync::Mutex;
use std::time::Instant;
use crate::launch::pump::{LineSink, LogLine};
use crate::launch::session::SessionWriter;
use crate::launch::window_detect::WindowDetector;
use crate::launch::window_detect::{shutdown_marker, WindowDetector};
/// 会话日志 sink。
#[derive(Debug)]
pub struct SessionSink {
writer: Mutex<SessionWriter>,
detector: Mutex<WindowDetector>,
shutdown_at: Mutex<Option<Instant>>,
last_line_at: Mutex<Option<Instant>>,
}
impl SessionSink {
/// 以进程启动时刻为窗口检测起点。
pub fn new(writer: SessionWriter) -> Self {
Self {
writer: Mutex::new(writer),
detector: Mutex::new(WindowDetector::new()),
}
Self::with_start(writer, Instant::now())
}
/// 指定窗口检测计时起点(进程 spawn 时刻)。
pub fn with_start(writer: SessionWriter, started_at: std::time::Instant) -> Self {
pub fn with_start(writer: SessionWriter, started_at: Instant) -> Self {
Self {
writer: Mutex::new(writer),
detector: Mutex::new(WindowDetector::with_start(started_at)),
shutdown_at: Mutex::new(None),
last_line_at: Mutex::new(None),
}
}
@@ -45,6 +47,16 @@ impl SessionSink {
Err(_) => false,
}
}
/// 第一次见到关闭标记(游戏自己说要退出)的时刻(D6)。
pub fn shutdown_seen_at(&self) -> Option<Instant> {
self.shutdown_at.lock().ok().and_then(|guard| *guard)
}
/// 最后一行的时刻(D6:还在打日志就说明还在加载,不算卡死)。
pub fn last_line_at(&self) -> Option<Instant> {
self.last_line_at.lock().ok().and_then(|guard| *guard)
}
}
impl LineSink for SessionSink {
@@ -53,9 +65,22 @@ impl LineSink for SessionSink {
Ok(mut detector) => detector.observe(&line.text),
Err(_) => None,
};
if let Ok(mut guard) = self.last_line_at.lock() {
*guard = Some(Instant::now());
}
if shutdown_marker(&line.text).is_some() {
if let Ok(mut guard) = self.shutdown_at.lock() {
guard.get_or_insert_with(Instant::now);
}
}
let Ok(mut writer) = self.writer.lock() else {
return;
};
// 会话已经收尾(看门狗放弃等待后进程还在打日志):不再往事件流里追加,
// 否则 `session_end` 之后会出现孤儿行。
if writer.is_finished() {
return;
}
if let Some(detection) = detection {
let _ = writer.window_detected(&detection);
}