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
@@ -105,10 +105,18 @@ pub fn run(ctx: &Ctx, args: &LaunchArgs) -> Result<Report, CliError> {
|
||||
..input
|
||||
};
|
||||
|
||||
let plan = snapshot::prepare(ctx, &input)?;
|
||||
// M188:命令行这一路也把启动器自己的运行日志落到 `<data>/logs/launcher.log`
|
||||
// (GUI 由 presenter 的 `HostEnv` 登记;两条路的落点必须是同一个)。
|
||||
// M188/M211:命令行这一路也把启动器日志写进 `<data>/logs/launcher.log`
|
||||
// (落点与格式的真相都在 core 的 `launcher_log`,GUI 写的是同一份)。
|
||||
unlucky_core::launch::launcher_log::set_dir(ctx.logs_dir());
|
||||
unlucky_core::launch::launcher_log::record_start(
|
||||
&input.instance_id,
|
||||
&isolation.game_dir,
|
||||
java.runtime
|
||||
.as_ref()
|
||||
.map(|runtime| (runtime.binary.as_path(), runtime.major.unwrap_or_default())),
|
||||
input.max_memory,
|
||||
);
|
||||
let plan = snapshot::prepare(ctx, &input)?;
|
||||
let config = LauncherConfig::new(ctx.sessions_root())
|
||||
.with_launcher(super::meta::LAUNCHER_NAME, env!("CARGO_PKG_VERSION"));
|
||||
let launcher = Launcher::new(config);
|
||||
@@ -187,6 +195,7 @@ pub fn run(ctx: &Ctx, args: &LaunchArgs) -> Result<Report, CliError> {
|
||||
let handle = launcher
|
||||
.launch(&plan.request)
|
||||
.map_err(|error| map_launch_error(&error, ctx))?;
|
||||
unlucky_core::launch::launcher_log::record_end(&input.instance_id, &handle);
|
||||
let data = json!({
|
||||
"dry_run": false,
|
||||
"instance_found": instance_found,
|
||||
|
||||
@@ -16,7 +16,6 @@ use unlucky_core::diagnose::collectors::{ExitSignal, SignalOrigin};
|
||||
use unlucky_core::diagnose::engine::{Diagnoser, DiagnosisInput};
|
||||
use unlucky_core::diagnose::loader::RuleLoader;
|
||||
use unlucky_core::launch::session_index::{self, SessionIndexEntry};
|
||||
use unlucky_core::platform::layout::GAME_LOG_DIR;
|
||||
|
||||
use crate::cli::{DiagArgs, LogsArgs};
|
||||
use crate::context::Ctx;
|
||||
@@ -241,14 +240,13 @@ fn find_entry(ctx: &Ctx, key: &str) -> Result<SessionIndexEntry, CliError> {
|
||||
fn event_path(ctx: &Ctx, entry: &SessionIndexEntry) -> std::path::PathBuf {
|
||||
// M188:新会话在 `logs/game/`,旧会话在 `data/agent/sessions/game/`(只读兼容)。
|
||||
// 先看新根,再回落旧家;两处都没有就把新根路径交出去(错误信息据此说「该去哪找」)。
|
||||
let primary = ctx.sessions_root().join(GAME_LOG_DIR).join(&entry.log_path);
|
||||
// M211/D2:`log_path` 已经**相对会话根**(`game/<id>.jsonl`),解析交给 core 里唯一的
|
||||
// `session_index::event_file`——这里不许再自己拼 `game/`,否则又是一份第三真相。
|
||||
let primary = session_index::event_file(&ctx.sessions_root(), &entry.log_path);
|
||||
if primary.is_file() {
|
||||
return primary;
|
||||
}
|
||||
let legacy = ctx
|
||||
.legacy_sessions_root()
|
||||
.join(GAME_LOG_DIR)
|
||||
.join(&entry.log_path);
|
||||
let legacy = session_index::event_file(&ctx.legacy_sessions_root(), &entry.log_path);
|
||||
if legacy.is_file() {
|
||||
return legacy;
|
||||
}
|
||||
@@ -339,3 +337,48 @@ fn exit_label(entry: &SessionIndexEntry) -> String {
|
||||
};
|
||||
format!("{code}/{exit_type}{silent}")
|
||||
}
|
||||
|
||||
/// M211/D5:`ull launch` 也必须写 `<data>/logs/launcher.log`(与 GUI 同一份文件、同一行格式)。
|
||||
///
|
||||
/// 为什么这条用例在这个文件里:CLI 侧本 mission 只允许改 `commands/launch.rs` 与
|
||||
/// `commands/sessions.rs`,而 `launch.rs` 已到 389 行、再加用例会破「文件 ≤400 行」——
|
||||
/// 用例放在同一个 `<data>/logs` 层级的读取方(本文件打印「启动器日志:<dir>」)旁边。
|
||||
///
|
||||
/// 变异证明:删掉 `commands/launch.rs` 里的 `record_start(...)` ⇒ 本条红(文件根本不存在);
|
||||
/// 把 `set_dir` 的入参换成别的目录 ⇒ 本条红。
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// 一个最小实例档案(`profiles.json`):只要能让 `launch` 走到「准备启动」这一段。
|
||||
const PROFILES: &str = r#"{"schema_version":1,"profiles":[{"id":"11111111-2222-4333-8444-000000000001","name":"生存服","version_id":"1.20.1"}]}"#;
|
||||
|
||||
#[test]
|
||||
fn a_launch_attempt_writes_the_launcher_log() {
|
||||
let root = std::env::temp_dir().join(format!("ull-m211-cli-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(root.join(".ULL/config")).expect("建配置目录");
|
||||
std::fs::write(root.join(".ULL/config/profiles.json"), PROFILES).expect("写 profiles");
|
||||
|
||||
let args: Vec<String> = vec![
|
||||
"launch".to_string(),
|
||||
"生存服".to_string(),
|
||||
"--data-dir".to_string(),
|
||||
root.display().to_string(),
|
||||
];
|
||||
let (mut out, mut err) = (Vec::new(), Vec::new());
|
||||
// 版本本体不存在 ⇒ 这条命令会失败,但「启动前」那一行必须已经落盘
|
||||
// (事故现场最需要的正是「它打算用什么起的」)。
|
||||
let _ = crate::run(&args, &mut out, &mut err);
|
||||
|
||||
let log = std::fs::read_to_string(root.join(".ULL/logs/launcher.log"))
|
||||
.expect("`ull launch` 必须在 <data>/logs/launcher.log 里留下启动器日志");
|
||||
assert!(
|
||||
log.contains("[launch] instance=11111111-2222-4333-8444-000000000001"),
|
||||
"{log}"
|
||||
);
|
||||
assert!(
|
||||
log.contains("game_dir=") && log.contains("memory="),
|
||||
"{log}"
|
||||
);
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user