fix(launch): 会话事件流路径只留一个真相源 + crash_found 只认真实证据 + ull launch 也写 launcher.log

M211(来自 M208 实机 e2e 的 D2/D3/D5):

- D2「game/ 双写」:log_path 的契约原来有两种说法(字段文档写「相对 game/」,写入侧一直
  写「相对会话根」的 game/<sid>.jsonl),于是两处读取点各自又多 join 一层 game/,
  ull logs / ull diag / GUI 日志页对每个真实会话都报「找不到事件流」。现在解析只走 core 的
  session_index::event_file(CLI 与 presenter 共用),写入侧只走
  session_index::log_path_for。方向选的是「读取侧不再 join」:存量索引里的 log_path 从第一版
  起就是 game/<id>.jsonl,改读取侧能把用户盘上已有的旧会话一起救回来(零迁移、零改写)。
- D2 夹具:cli/tests/support 与 presenter/host/logs_tests 的索引夹具改成生产形状
  (game/<id>.jsonl)。夹具此前自己说了第三种形状,所以这个 bug 在测试里永远看不见——
  修夹具比修路径更重要。
- D3「crash_found 恒为 true」:logs/latest.log 每局都写,不再被当现场文件(只在已经找到
  权威现场时作为补充证据)。崩溃结论改由 crash_scan::has_crash_evidence 判定(权威现场文件
  / 非 0 退出 + 错误行),索引旗标由 SessionWriter::mark_crash 写入;crash_found 事件恢复
  成纯事实记录。
- D5「ull launch 从不写 launcher.log」:现在与 GUI 共用
  launcher_log::record_start / record_end(字段名与顺序只在一个地方定义),写的是同一份
  <data>/logs/launcher.log。

测试:core 新增 session_path_tests(写入形状 = 读取解析结果 = 真落点的往返)、crash_scan 的
干净退出用例、launcher_log 的行格式锁;CLI 新增 a_launch_attempt_writes_the_launcher_log。
This commit is contained in:
AskaEth committed 2026-11-09 14:12:23 +08:00
1 parent 1748e046f8
commit 4b30bf07f0
12 files changed
+542 -44

No files matched your search

+105
View File
@@ -29,6 +29,7 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use super::launch_session::SessionHandle;
use super::redact::Redactor;
/// 日志文件名。
@@ -102,6 +103,83 @@ pub fn record(scope: &str, message: &str) {
append(&dir, &line);
}
/// 一次启动的「启动前」一行:`instance=… game_dir=… java=… memory=…MB`(M211/D5)。
///
/// GUI(`presenter::host::launch`)与 CLI(`ull launch`)写的是**同一份** `launcher.log`,
/// 所以字段名/顺序只在这里定义一次——两边各写一句 `format!` 就等于迟早漂成两种形状。
fn start_line(
instance_id: &str,
game_dir: &Path,
java: Option<(&Path, u32)>,
memory_mb: Option<u32>,
) -> String {
let java = java.map_or_else(
|| "未找到".to_string(),
|(binary, major)| java_label(binary, major),
);
format!(
"instance={instance_id} game_dir={} java={java} memory={}MB",
game_dir.display(),
memory_mb.unwrap_or(0)
)
}
/// `java=` 字段的形状:`<二进制> (major <主版本>)`。
pub fn java_label(binary: &Path, major: u32) -> String {
format!("{} (major {major})", binary.display())
}
/// 一次启动的「收尾」一行:`instance=… session=… exit=… type=… uptime=…ms outcome=… artifacts=…`。
///
/// `outcome` 与 GUI 的 `LogOutcome` 同词(`Ok` / `Crashed` / `SilentFail`),见
/// [`SessionHandle::outcome_label`]。
#[allow(clippy::too_many_arguments)]
fn end_line(
instance_id: &str,
session_id: &str,
exit_code: i32,
exit_type: &str,
uptime_ms: u64,
outcome: &str,
artifacts: usize,
) -> String {
format!(
"instance={instance_id} session={session_id} exit={exit_code} type={exit_type} \
uptime={uptime_ms}ms outcome={outcome} artifacts={artifacts}"
)
}
/// 记「启动前」一行(`instance` / `game_dir` / `java` / `memory`)。
///
/// 装配失败也要留下这一行:事故之后「到底打算用什么起的」是第一个要看的。
pub fn record_start(
instance_id: &str,
game_dir: &Path,
java: Option<(&Path, u32)>,
memory_mb: Option<u32>,
) {
record(
"launch",
&start_line(instance_id, game_dir, java, memory_mb),
);
}
/// 记「收尾」一行(`session` / `exit` / `type` / `uptime` / `outcome` / `artifacts`)。
pub fn record_end(instance_id: &str, handle: &SessionHandle) {
record(
"launch",
&end_line(
instance_id,
&handle.session_id,
handle.exit.code,
handle.exit.exit_type.as_str(),
handle.uptime_ms,
handle.outcome_label(),
handle.artifacts.len(),
),
);
}
/// 脱敏器:主目录换成 `<home>`(同一份规则见 [`super::redact`])。
fn redactor() -> Redactor {
let mut redactor = Redactor::new();
@@ -204,4 +282,31 @@ mod tests {
assert!(lines <= MAX_BUFFERED, "缓冲没有上限:{lines} 行");
assert!(lines > 0);
}
/// M211/D5:GUI 与 `ull launch` 写**同一份** `launcher.log`,所以形状只能有一套。
///
/// 变异证明:把 `start_line`/`end_line` 里的键名或顺序改掉(例如 `memory` → `mem`)⇒
/// 本条的字符串相等断言红——这正是「两条路各自 `format!`」时会发生的事。
#[test]
fn launch_lines_have_one_shape_for_both_entries() {
use std::path::Path;
assert_eq!(
super::start_line(
"inst",
Path::new("C:/mc"),
Some((Path::new("C:/jdk/bin/java.exe"), 17)),
Some(4096),
),
"instance=inst game_dir=C:/mc java=C:/jdk/bin/java.exe (major 17) memory=4096MB"
);
assert_eq!(
super::start_line("inst", Path::new("C:/mc"), None, None),
"instance=inst game_dir=C:/mc java=未找到 memory=0MB"
);
assert_eq!(
super::end_line("inst", "sid", 1, "application_error", 1234, "Crashed", 2),
"instance=inst session=sid exit=1 type=application_error \
uptime=1234ms outcome=Crashed artifacts=2"
);
}
}