Files
Unlucky-Launcher/crates/core/src/launch/session_handle.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

131 lines
4.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 一次启动的结果([`SessionHandle`])。
//!
//! 从 `launch_session` 拆出:那边是「跑一局 + 收尾」的流水线,这里是**纯数据 + 展示**。
//! 拆开的直接原因是行数——`launch_session` 加上 D6 的有界等待后越过了 400 行红线。
use std::path::PathBuf;
use serde_json::Value;
use crate::launch::args_table::AssembledCommand;
use crate::launch::crash_scan::CrashArtifact;
use crate::launch::exit_analysis::ExitAnalysis;
use crate::launch::session_index::SessionIndexEntry;
/// 一次启动的结果。
#[derive(Debug, Clone)]
pub struct SessionHandle {
/// 会话 id。
pub session_id: String,
/// 事件流文件。
pub log_path: PathBuf,
/// 组装结果(`argv()` 含凭据明文;对外展示/上报请用 [`Self::redacted_argv`])。
pub command: AssembledCommand,
/// 脱敏后的完整 argv(accessToken → `<redacted:token>`,主目录 → `<home>`)。
redacted_argv: Vec<String>,
/// 退出判定。
pub exit: ExitAnalysis,
/// 诊断结论(M15 产出,已写入事件流)。
pub diagnostics: Option<Value>,
/// 索引条目(写入 `index.jsonl` 的那一行)。
pub index: Option<SessionIndexEntry>,
/// 崩溃现场文件。
pub artifacts: Vec<CrashArtifact>,
/// 游戏存活毫秒数。
pub uptime_ms: u64,
/// 日志泵是否超时(watchdog 触发 = 有子进程没关 stdout)。
pub pump_stalled: bool,
/// 会话写入错误(磁盘问题;不影响进程结果)。
pub event_write_errors: Vec<String>,
}
impl SessionHandle {
/// 构造(`redacted_argv` 只在这里赋值,外部拿不到明文 argv 的那条路)。
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
session_id: String,
log_path: PathBuf,
command: AssembledCommand,
redacted_argv: Vec<String>,
exit: ExitAnalysis,
diagnostics: Option<Value>,
index: Option<SessionIndexEntry>,
artifacts: Vec<CrashArtifact>,
uptime_ms: u64,
pump_stalled: bool,
event_write_errors: Vec<String>,
) -> Self {
Self {
session_id,
log_path,
command,
redacted_argv,
exit,
diagnostics,
index,
artifacts,
uptime_ms,
pump_stalled,
event_write_errors,
}
}
/// 脱敏后的完整 argv(UI/日志/上报都用它,别直接外发 `command.argv()`)。
pub fn redacted_argv(&self) -> &[String] {
&self.redacted_argv
}
/// 脱敏后的一行人读命令行。
pub fn redacted_display(&self) -> String {
self.redacted_argv
.iter()
.map(|arg| crate::launch::command_builder::quote_for_display(arg))
.collect::<Vec<_>>()
.join(" ")
}
/// 是否“正常且无异常”:退出判定 NORMAL 且没有静默失败标记。
pub fn succeeded(&self) -> bool {
!self.exit.exit_type.is_abnormal() && self.exit.silent_failure.is_none()
}
/// 是否需要进诊断(等价于 `exit.needs_diagnosis`,保留给上层判空)。
pub fn needs_diagnosis(&self) -> bool {
self.exit.needs_diagnosis
}
/// 结局标签,与 GUI 的 `LogOutcome` 同词(`Ok` / `Crashed` / `SilentFail`)。
///
/// `launcher.log` 里两条路(GUI / `ull launch`)都写这一套词,见
/// [`crate::launch::launcher_log::launch_end_line`]。
pub fn outcome_label(&self) -> &'static str {
if self.exit.silent_failure.is_some() {
"SilentFail"
} else if self.succeeded() {
"Ok"
} else {
"Crashed"
}
}
/// 一句话摘要。
pub fn summary(&self) -> String {
let mut text = format!("会话 {}:{}", self.session_id, self.exit.summary());
if self.pump_stalled {
text.push_str(";日志泵超时");
}
if let Some(top) = &self.diagnostics {
if let Some(id) = top
.get("candidates")
.and_then(Value::as_array)
.and_then(|items| items.first())
.and_then(|item| item.get("id"))
.and_then(Value::as_str)
{
text.push_str(&format!(";诊断首因 {id}"));
}
}
text
}
}