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。
90 lines
3.1 KiB
Rust
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);
|
|
}
|
|
}
|