Files
Unlucky-Launcher/crates/core/src/launch/session_sink.rs
T
AskaEth ff78fe2cae 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。
2026-11-09 17:07:22 +08:00

90 lines
3.1 KiB
Rust

//! 把会话事件流接到日志泵上:逐行落盘 + 自动窗口出现检测。
//!
//! 放在单独文件是为了让 [`crate::launch::session`] 保持“只写事件、只管索引”的单一职责,
//! 也避免单文件超过 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::{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::with_start(writer, Instant::now())
}
/// 指定窗口检测计时起点(进程 spawn 时刻)。
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),
}
}
/// 借用写入器(退出分析、结束会话等需要可变访问的场合)。
pub fn with_writer<R>(&self, action: impl FnOnce(&mut SessionWriter) -> R) -> Option<R> {
self.writer.lock().ok().map(|mut guard| action(&mut guard))
}
/// 是否已检测到游戏窗口。
pub fn window_detected(&self) -> bool {
match self.writer.lock() {
Ok(writer) => writer.is_window_detected(),
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 {
fn on_line(&self, line: &LogLine) {
let detection = match self.detector.lock() {
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);
}
let _ = writer.line(line);
}
}