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
@@ -259,11 +259,7 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
};
|
||||
report.run_dir = request.dirs.game_dir.clone();
|
||||
report.version_json = Some(env.game.version_json(&target.version_id));
|
||||
report.java = format!(
|
||||
"{} (major {})",
|
||||
request.java.binary.display(),
|
||||
request.java.major
|
||||
);
|
||||
report.java = launcher_log::java_label(&request.java.binary, request.java.major);
|
||||
for warning in &warnings {
|
||||
let _ = tx.send(HostTask::Notice(Box::new(crate::dto::NoticeView::new(
|
||||
&format!("launch-warn:{}", target.id),
|
||||
@@ -273,14 +269,13 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
))));
|
||||
}
|
||||
// 命令行(脱敏版)进启动器日志:事故之后「到底用什么参数起的」是第一个要看的。
|
||||
let line = format!(
|
||||
"instance={} game_dir={} java={} memory={}MB",
|
||||
target.id,
|
||||
report.run_dir.display(),
|
||||
report.java,
|
||||
request.memory.max_mb.unwrap_or(0)
|
||||
// M211/D5:`ull launch` 写同一个文件的同一行(格式在 core 的 `launcher_log` 里)。
|
||||
launcher_log::record_start(
|
||||
&target.id,
|
||||
&report.run_dir,
|
||||
Some((request.java.binary.as_path(), request.java.major)),
|
||||
request.memory.max_mb,
|
||||
);
|
||||
launcher_log::record("launch", &line);
|
||||
let config = LauncherConfig::new(env.sessions_root())
|
||||
.with_launcher(LAUNCHER_NAME, env!("CARGO_PKG_VERSION"));
|
||||
let session = match Launcher::new(config).spawn(&request) {
|
||||
@@ -317,16 +312,7 @@ pub fn start(env: HostEnv, tx: JobSender, target: LaunchTarget) {
|
||||
match session.finish() {
|
||||
Ok(handle) => {
|
||||
let outcome = super::identity::outcome_of(&handle);
|
||||
let line = format!(
|
||||
"instance={} session={session_id} exit={} type={} uptime={}ms outcome={:?} artifacts={}",
|
||||
target.id,
|
||||
handle.exit.code,
|
||||
handle.exit.exit_type.as_str(),
|
||||
handle.uptime_ms,
|
||||
outcome,
|
||||
handle.artifacts.len()
|
||||
);
|
||||
launcher_log::record("launch", &line);
|
||||
launcher_log::record_end(&target.id, &handle);
|
||||
report.secrets.push(session_id.clone());
|
||||
let lines: Vec<String> = super::logs::read_lines(&handle.log_path)
|
||||
.into_iter()
|
||||
|
||||
@@ -20,8 +20,7 @@ use serde_json::Value;
|
||||
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::SessionIndexEntry;
|
||||
use unlucky_core::platform::layout::GAME_LOG_DIR;
|
||||
use unlucky_core::launch::session_index::{self, SessionIndexEntry};
|
||||
|
||||
use crate::dto::{
|
||||
Diagnosis, DiagnosisReport, LogLevel, LogLine, LogOutcome, LogSession, SessionId,
|
||||
@@ -80,16 +79,20 @@ pub fn display_time(started_at: &str) -> String {
|
||||
format!("{date} {time}")
|
||||
}
|
||||
|
||||
/// 事件流文件路径(`<logs>/game/<log_path>`;新根没有时回落到旧家)。
|
||||
/// 事件流文件路径(`<logs>/<log_path>`;新根没有时回落到旧家)。
|
||||
///
|
||||
/// `log_path` 由写入侧按**相对会话根**的形状产出(`game/<id>.jsonl`,见
|
||||
/// [`session_index::log_path_for`]),解析只走 [`session_index::event_file`] 一处——
|
||||
/// CLI 与 GUI 两个读取点共用它,谁也不许再自己拼一层 `game/`(M211/D2)。
|
||||
///
|
||||
/// 两处都找不到就返回**新根**的路径:调用方据此说出「该去哪找」,而不是一个旧目录路径。
|
||||
pub fn event_path(env: &HostEnv, entry: &SessionIndexEntry) -> PathBuf {
|
||||
let primary = root(env).join(GAME_LOG_DIR).join(&entry.log_path);
|
||||
let primary = session_index::event_file(&root(env), &entry.log_path);
|
||||
if primary.is_file() {
|
||||
return primary;
|
||||
}
|
||||
if let Some(legacy) = legacy_root(env) {
|
||||
let fallback = legacy.join(GAME_LOG_DIR).join(&entry.log_path);
|
||||
let fallback = session_index::event_file(&legacy, &entry.log_path);
|
||||
if fallback.is_file() {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@ fn temp_root(tag: &str) -> PathBuf {
|
||||
root
|
||||
}
|
||||
|
||||
/// 索引条目夹具。`log_path` **必须是生产写入的形状**(相对会话根:`game/<id>.jsonl`)。
|
||||
///
|
||||
/// M211/D2:这里以前传不带 `game/` 的 `<id>.jsonl`,于是「读取侧多 join 一层 `game/`」的 bug
|
||||
/// 在夹具里永远看不见——夹具和生产各自自洽,两边都是绿的。改夹具比改路径更重要。
|
||||
fn sample_entry(id: &str, file: &str, started_at: &str) -> SessionIndexEntry {
|
||||
SessionIndexEntry {
|
||||
v: 1,
|
||||
@@ -129,7 +133,7 @@ fn legacy_sessions_are_still_listed_and_openable() {
|
||||
std::fs::create_dir_all(&legacy_game).expect("建旧家目录");
|
||||
let entry = sample_entry(
|
||||
"old-session",
|
||||
"old-session.jsonl",
|
||||
"game/old-session.jsonl",
|
||||
"2026-11-01T10:00:00.000Z",
|
||||
);
|
||||
session_index::append(
|
||||
@@ -174,7 +178,7 @@ fn new_sessions_are_written_under_logs_game() {
|
||||
let index = session_index::index_path(&env.sessions_root());
|
||||
assert_eq!(index, root.join(".ULL/logs/game/index.jsonl"));
|
||||
|
||||
let entry = sample_entry("fresh", "fresh.jsonl", "2026-11-08T09:00:00.000Z");
|
||||
let entry = sample_entry("fresh", "game/fresh.jsonl", "2026-11-08T09:00:00.000Z");
|
||||
session_index::append(&index, &entry).expect("写新索引");
|
||||
let file = env.sessions_root().join("game/fresh.jsonl");
|
||||
std::fs::write(
|
||||
@@ -202,7 +206,7 @@ fn the_new_root_wins_when_both_indexes_know_the_same_id() {
|
||||
let env = HostEnv::at_root(&root);
|
||||
let legacy_game = env.legacy_sessions_root().join("game");
|
||||
std::fs::create_dir_all(&legacy_game).expect("建旧家目录");
|
||||
let old = sample_entry("same", "same.jsonl", "2026-10-01T00:00:00.000Z");
|
||||
let old = sample_entry("same", "game/same.jsonl", "2026-10-01T00:00:00.000Z");
|
||||
session_index::append(
|
||||
&session_index::index_path(&env.legacy_sessions_root()),
|
||||
&old,
|
||||
@@ -214,7 +218,7 @@ fn the_new_root_wins_when_both_indexes_know_the_same_id() {
|
||||
)
|
||||
.expect("写旧事件流");
|
||||
|
||||
let fresh = sample_entry("same", "same.jsonl", "2026-11-08T00:00:00.000Z");
|
||||
let fresh = sample_entry("same", "game/same.jsonl", "2026-11-08T00:00:00.000Z");
|
||||
session_index::append(&session_index::index_path(&env.sessions_root()), &fresh)
|
||||
.expect("写新索引");
|
||||
let new_file = env.sessions_root().join("game/same.jsonl");
|
||||
|
||||
Reference in new issue
Block a user