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:
1 parent
1748e046f8
commit
4b30bf07f0
12 files changed
+542
-44
No files matched your search
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user