From 8c5bebf3e395c3a60963b8f65cd1d75c4ca2ec1e Mon Sep 17 00:00:00 2001 From: worker-logplace Date: Mon, 9 Nov 2026 01:05:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(m188):=20r1=20=E5=A4=8D=E5=AE=A1=20P2-1?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=90=8E=E7=AB=AF=E5=90=AF=E5=8A=A8=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=9A=84=E5=85=B7=E5=90=8D=E5=8E=9F=E5=9B=A0=E8=BF=9B?= =?UTF-8?q?=20launcher.log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复审结论 p2-1/1 item + fix-then-merge:主线成立,但「启动器自己因 GPU / 渲染后端起不来」的 事故原因仍只进 stderr —— 双击启动没有 stderr,那条路径上等于没有日志。 - renderer/mod.rs::report_failure 的两行(`[gpu] 初始化失败(具名)` / `[gpu] 失败详情(具名)`)改走 diag!; - renderer/log_bridge.rs 转出的 wgpu 日志(含 `…Selected: AdapterInfo…`,GPU 真生效的唯一证据 行)改走 diag!; - renderer/preflight.rs 的 `[gpu-preflight]`(ULL_PREFLIGHT=1)改走 diag!; - probe.rs::apply_with_preflight 的三行 `[probe]` 改走 diag!。 - app/diag.rs 模块文档 + docs/logs-location.md §3 的判据改准: 「逐帧度量不接;显式取证开关与一次性事故行可接」,并修掉 `[corner]` 两处自相矛盾 (唯一打印点在 app/update/appearance.rs,走 diag! ⇒ 进日志;corner.rs 只 format_line)。 - 新守门 renderer::tests::a_backend_startup_failure_lands_in_the_launcher_log: 断言那两行真的落进 launcher.log;变异(改回纯 eprintln)→ 红。 - README 两语的 launcher.log 行补上「渲染后端起不来的具名原因」。 --- README.md | 2 +- README.zh-CN.md | 2 +- crates/app-iced/src/app/diag.rs | 14 ++++--- crates/app-iced/src/app/mod.rs | 2 +- crates/app-iced/src/probe.rs | 9 +++-- crates/app-iced/src/renderer/log_bridge.rs | 4 +- crates/app-iced/src/renderer/mod.rs | 47 +++++++++++++++++++++- crates/app-iced/src/renderer/preflight.rs | 8 +++- docs/logs-location.md | 26 +++++++----- 9 files changed, 87 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 386373a..1c23e9e 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ as the launcher data directory. | Path | What it is | |---|---| -| `logs/launcher.log` | The launcher's own runtime log: boot probe, font/theme decisions, backend choice, and every launch attempt (command line facts, spawn result, exit). Rolled over at 1 MiB into `logs/launcher.1.log`, exactly **one** old copy kept, so it cannot grow without bound. | +| `logs/launcher.log` | The launcher's own runtime log: boot probe, font/theme decisions, backend choice, **the named reason when a render backend fails to start**, and every launch attempt (command line facts, spawn result, exit). Rolled over at 1 MiB into `logs/launcher.1.log`, exactly **one** old copy kept, so it cannot grow without bound. | | `logs/launcher.1.log` | The previous 1 MiB, replaced on the next rollover. | | `logs/game/.jsonl` | One JSONL event stream per launch: the command line (secrets redacted), every stdout/stderr line, exit code. | | `logs/game/index.jsonl` | The one-line-per-session index the logs page lists. | diff --git a/README.zh-CN.md b/README.zh-CN.md index e931918..9742baa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -178,7 +178,7 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 | 路径 | 是什么 | |---|---| -| `logs/launcher.log` | 启动器自己的运行日志:后端探测、字体/主题决策、后端选择,以及每一次启动(命令行事实、spawn 结果、退出码)。满 1 MiB 滚成 `logs/launcher.1.log`,**只留一份**旧档,所以它不会无限长大。 | +| `logs/launcher.log` | 启动器自己的运行日志:后端探测、字体/主题决策、后端选择、**渲染后端起不来时的具名原因**,以及每一次启动(命令行事实、spawn 结果、退出码)。满 1 MiB 滚成 `logs/launcher.1.log`,**只留一份**旧档,所以它不会无限长大。 | | `logs/launcher.1.log` | 上一份 1 MiB,下次滚动时被替换。 | | `logs/game/.jsonl` | 每次启动一份 JSONL 事件流:命令行(已脱敏)、每一行 stdout/stderr、退出码。 | | `logs/game/index.jsonl` | 一会话一行摘要的索引,日志页左栏读的就是它。 | diff --git a/crates/app-iced/src/app/diag.rs b/crates/app-iced/src/app/diag.rs index ade7352..ffb2693 100644 --- a/crates/app-iced/src/app/diag.rs +++ b/crates/app-iced/src/app/diag.rs @@ -17,16 +17,18 @@ //! //! ## 哪些行进日志、哪些只进 stderr //! -//! 判据只有一条:**事故之后还要不要看它**。 +//! 判据(M188 复审后收紧成这一句):**逐帧度量不接;显式取证开关与一次性事故行可接。** //! //! | 进日志 | 只进 stderr | //! |---|---| -//! | 启动编排(探测结论 / 字体重建 / 主题来源 / 后端最终值 / 降级重开) | 验收与出图通道的逐帧度量(`[window]` / `[perf]` / `[caps]`——那些行由 `renderer::acceptance` 打,不在本 mission scope) | -//! | 用户动作的结果(打开目录、写设置失败、更新忽略/应用、语言没落盘) | 组件几何探针(`[feedback-probe]` / `[gallery]` 的断言输出,`components/**`) | -//! | 一次性且量很小的诊断(`[appearance]` / `[corner]` 的切换、`[theme]`、`[shot]` 的通道提示) | 每条日志行的正文里再套一层括号(不是标签,见 [`split_scope`]) | +//! | 启动编排(探测结论 / 字体重建 / 主题来源 / 后端最终值 / 降级重开) | **验收与出图矩阵的逐帧度量**:`[window]` / `[perf]` / `[caps]` / `[theme] source=`(`renderer::acceptance/**`,一次启动上百行) | +//! | **一次性事故行**:`[gpu] 初始化失败(具名)` / `[gpu] 失败详情(具名)`(`renderer::report_failure`)、`[gpu] iced_wgpu: …Selected: AdapterInfo…`(`log_bridge`)、`[probe] request=… -> ICED_BACKEND=…`(`probe::apply_with_preflight`)、`[gpu-preflight]`(`ULL_PREFLIGHT=1`) | 组件几何探针的断言输出(`components/**` 的 `[feedback-probe]` / gallery 页面里的 `[gallery]`) | +//! | 用户动作的结果(打开目录、写设置失败、更新忽略/应用、语言没落盘) | 每条日志行的正文里再套一层括号(不是标签,见 [`split_scope`]) | +//! | 显式开关下的一次性行:`[tick]`(`ULL_TICK_TRACE=1`,≈1 行/秒 ≈ 4.8 小时才滚一次)、`[shot]` 的通道提示 | — | //! -//! 反过来**不许**把高频行接进来:`launcher.log` 有 1 MiB 上限(滚成 `launcher.1.log`,只留一份), -//! 逐帧写会把「事故现场」冲掉。 +//! 为什么逐帧行不许接:`launcher.log` 有 1 MiB 上限(滚成 `launcher.1.log`,只留一份), +//! 逐帧写会在几分钟内把「事故现场」冲掉。反过来,`report_failure` 那两行**就是**事故本体—— +//! 双击启动没有 stderr,把它们留在 stderr 等于在最该有日志的路径上什么都没有(M188 复审 P2-1)。 /// 行首没有 `[tag]` 时的默认 scope。 pub const APP_SCOPE: &str = "app"; diff --git a/crates/app-iced/src/app/mod.rs b/crates/app-iced/src/app/mod.rs index dec5277..4e2a686 100644 --- a/crates/app-iced/src/app/mod.rs +++ b/crates/app-iced/src/app/mod.rs @@ -30,7 +30,7 @@ mod appearance; mod backend; mod boot; -mod diag; +pub(crate) mod diag; mod dialog; pub(crate) mod logs_folder; mod message; diff --git a/crates/app-iced/src/probe.rs b/crates/app-iced/src/probe.rs index 3b53de0..2755801 100644 --- a/crates/app-iced/src/probe.rs +++ b/crates/app-iced/src/probe.rs @@ -119,15 +119,18 @@ pub fn apply_with_preflight( "" }; - eprintln!( + // M188 复审 P2-1:这三行是**一次性的启动取证**(后端请求 → 落定的 ICED_BACKEND → 取证指南), + // 必须进 `<.ULL>/logs/launcher.log`:双击启动没有 stderr,否则「为什么用这个后端 / 为什么降级」 + // 在事故之后完全不可复原。 + crate::app::diag::diag!( "[probe] {} :: request={} -> ICED_BACKEND={}{note}", probe.describe(), request.name(), decision.primary.env_value() ); - eprintln!("[probe] {}", decision.describe()); + crate::app::diag::diag!("[probe] {}", decision.describe()); if decision.primary == renderer::Kind::Wgpu { - eprintln!( + crate::app::diag::diag!( "[probe] 取证:出现 `[gpu] iced_wgpu: …Selected: AdapterInfo…` ⇒ GPU 真生效;\ 出现 `[gpu] wgpu 初始化失败` ⇒ 已降级 CPU" ); diff --git a/crates/app-iced/src/renderer/log_bridge.rs b/crates/app-iced/src/renderer/log_bridge.rs index d45f9a6..5b23e1d 100644 --- a/crates/app-iced/src/renderer/log_bridge.rs +++ b/crates/app-iced/src/renderer/log_bridge.rs @@ -88,7 +88,9 @@ impl log::Log for Bridge { } // iced_wgpu 用 `{:#?}` 打多行结构体;压成一行才好 grep / 贴验收报告。 let message = record.args().to_string().replace('\n', " "); - eprintln!("[gpu] {}: {}", record.target(), message.trim_end()); + // M188 复审 P2-1:`[gpu] iced_wgpu: …Selected: AdapterInfo…` 是「GPU 真生效」的**唯一** + // 证据行,也是「为什么降级了」的第一现场,必须进 `<.ULL>/logs/launcher.log`。 + crate::app::diag::diag!("[gpu] {}: {}", record.target(), message.trim_end()); } fn flush(&self) {} diff --git a/crates/app-iced/src/renderer/mod.rs b/crates/app-iced/src/renderer/mod.rs index 90ea616..46d9121 100644 --- a/crates/app-iced/src/renderer/mod.rs +++ b/crates/app-iced/src/renderer/mod.rs @@ -199,9 +199,12 @@ pub fn forced_fallback() -> Option { /// `iced::Error::GraphicsCreationFailed` 的 `Display` **不包含**内部错误 /// (`#[error("the application graphics context could not be created")]`,没有 `{0}`), /// 所以这里必须显式把内层错误打出来,否则又变回「只知道失败,不知道为什么」。 +/// +/// **M188 复审 P2-1**:这两行是**事故本体**,必须同时进 `<.ULL>/logs/launcher.log`—— +/// 双击启动的 GUI 进程没有 stderr,只打 `eprintln!` 等于「最该有日志的那条路径上没有日志」。 pub fn report_failure(backend: Kind, error: &(impl std::fmt::Display + std::fmt::Debug)) { - eprintln!("[gpu] {} 初始化失败(具名):{error}", backend.name()); - eprintln!("[gpu] 失败详情(具名):{error:?}"); + crate::app::diag::diag!("[gpu] {} 初始化失败(具名):{error}", backend.name()); + crate::app::diag::diag!("[gpu] 失败详情(具名):{error:?}"); } #[cfg(test)] @@ -246,4 +249,44 @@ mod tests { }); assert_eq!(calls, 1, "进程内只许跑一次;重试只能换进程"); } + + /// **M188 复审 P2-1 的守门**:后端起不来那两行必须进 `<.ULL>/logs/launcher.log`,不只是 stderr。 + /// + /// 为什么这条最要紧:双击启动的 GUI 进程**没有 stderr 去向**,而「GPU / 渲染后端起不来」 + /// 正是最需要事后复盘的一类事故。只打 `eprintln!` 等于在这条路径上什么都没留下—— + /// 用户那句「崩了但只有一个安装日志」会在最该有日志的地方继续成立。 + /// + /// 抓得住:`report_failure` 退回纯 `eprintln!`(变异证明:改回去本条立刻红)。 + /// 抓不住:真机上 iced 起不来的样子(那要真 GPU/驱动,见报告「没验的部分」)。 + #[test] + fn a_backend_startup_failure_lands_in_the_launcher_log() { + use crate::app::diag::LOG_STATE_LOCK; + use unlucky_core::launch::launcher_log; + + // `launcher_log` 是进程级单例:与 `app/diag.rs` 的滚动闸、`app/dialog/tests.rs` 的启动事实 + // 用例串行(同一个 `LOG_STATE_LOCK`)。 + let _guard = LOG_STATE_LOCK + .lock() + .unwrap_or_else(|error| error.into_inner()); + let dir = std::env::temp_dir().join(format!("ull-m188-gpu-fail-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("建夹具目录"); + launcher_log::set_dir(&dir); + + report_failure(Kind::Wgpu, &"no suitable adapter (host: test)".to_string()); + + let path = dir.join(launcher_log::FILE_NAME); + let text = std::fs::read_to_string(&path).expect("launcher.log"); + assert!( + text.contains("[gpu] wgpu 初始化失败(具名):no suitable adapter"), + "具名原因没进日志:{text}" + ); + assert!( + text.contains("[gpu] 失败详情(具名):"), + "失败详情没进日志:{text}" + ); + // 取证:`cargo test … -- --nocapture` 时把真实落盘内容打出来(报告/复审可复核)。 + println!("launcher.log:\n{text}"); + std::fs::remove_dir_all(&dir).ok(); + } } diff --git a/crates/app-iced/src/renderer/preflight.rs b/crates/app-iced/src/renderer/preflight.rs index ff6b101..a7c2276 100644 --- a/crates/app-iced/src/renderer/preflight.rs +++ b/crates/app-iced/src/renderer/preflight.rs @@ -76,10 +76,14 @@ pub fn run_if_requested() -> Option> { let started = std::time::Instant::now(); let verdict = run(); let took = started.elapsed().as_secs_f64() * 1000.0; + // M188:显式开关(`ULL_PREFLIGHT=1`)下的一次性行,按 `app::diag` 的判据可以进 + // `launcher.log`——用户开这个开关就是为了拿到「为什么起不来」的证据。 match &verdict { - Ok(ok) => eprintln!("[gpu-preflight] {} took={took:.0}ms", ok.describe()), + Ok(ok) => { + crate::app::diag::diag!("[gpu-preflight] {} took={took:.0}ms", ok.describe()); + } Err(failure) => { - eprintln!("[gpu-preflight] {} took={took:.0}ms", failure.describe()); + crate::app::diag::diag!("[gpu-preflight] {} took={took:.0}ms", failure.describe()); } } Some(verdict) diff --git a/docs/logs-location.md b/docs/logs-location.md index 7eb2e30..7f790c0 100644 --- a/docs/logs-location.md +++ b/docs/logs-location.md @@ -41,20 +41,26 @@ GUI 的 `--data-dir ` 是 custom 模式的一种入口:那时日志根是 ## 3. 哪些行进 launcher.log、哪些只进 stderr -判据只有一条:**事故之后还要不要看它**。实现是 `crates/app-iced/src/app/diag.rs` 的 `diag!` 宏 -(stderr + 文件双写,行首 `[tag]` 就是日志里的 scope)。 +判据(复审后收紧成这一句):**逐帧度量不接;显式取证开关与一次性事故行可接**。实现是 +`crates/app-iced/src/app/diag.rs` 的 `diag!` 宏(stderr + 文件双写,行首 `[tag]` 就是日志里的 scope)。 -| 进 launcher.log(`crates/app-iced/src/app/**` 里打的行) | 只进 stderr(本 mission scope 之外的行) | +| 进 launcher.log | 只进 stderr | |---|---| -| 启动编排:`[probe]`(降级重开 / 探测耗时)、`[font]`、`[theme]`、`[appearance]`、`[gpu]` 的降级与拒绝降级、`[boot]`(含后端 request/effective)、`[shot]` 的通道提示 | `renderer::acceptance` 的验收度量:`[window]` / `[perf]` / `[caps]` / `[theme] source=…` | -| 用户动作的结果:`[logs]`(打开日志文件夹成功/失败)、`[settings]` 写盘失败、`[i18n]` 语言没落盘、`[update]` 的忽略/重启、`[agent]` / `[content]` / `[page]` 的落点提示 | `renderer` 自己的 `[gpu] …初始化失败`、`[gpu] iced_wgpu: Selected`、`[probe] request=…`(`probe.rs`)、`[corner]`(`platform/win32/corner.rs`) | -| 一次性、量很小的诊断(每次启动几行到几十行) | 组件几何探针与测试:`[feedback-probe]` / `[gallery]` / `[probe] divider…` | +| 启动编排:`[probe]`(`request=… -> ICED_BACKEND=…`、降级重开、探测耗时)、`[font]`、`[theme]`、`[appearance]`、`[gpu]` 的降级与拒绝降级、`[boot]`(含后端 request/effective)、`[shot]` 的通道提示 | 验收/出图矩阵的**逐帧度量**:`[window]` / `[perf]` / `[caps]` / `[theme] source=…`(`renderer::acceptance/**`,一次启动上百行,会冲掉事故现场) | +| **一次性事故行**:`[gpu] 初始化失败(具名)` 与 `[gpu] 失败详情(具名)`(`renderer::report_failure`,复审 P2-1)、`[gpu] : …Selected: AdapterInfo…`(`renderer/log_bridge.rs` 转出的 wgpu 日志)、`[gpu-preflight]`(`ULL_PREFLIGHT=1`) | 组件几何探针的断言输出(`components/**` 的 `[feedback-probe]`、gallery 页面里的 `[gallery]`) | +| 用户动作的结果:`[logs]`(打开日志文件夹成功/失败)、`[settings]` 写盘失败、`[i18n]` 语言没落盘、`[update]` 的忽略/重启、`[agent]` / `[content]` / `[page]` 的落点提示 | 每条日志行的正文里再套的括号(不是标签) | +| 显式开关下的一次性行:`[tick]`(`ULL_TICK_TRACE=1`,≈1 行/秒,约 4.8 小时才滚一次)、`[corner]`(`app/update/appearance.rs` 打,`platform/win32/corner.rs` 只负责 `format_line`)、`[appearance]` / `[theme]` 的切换 | — | -不允许把高频行接进来:1 MiB 的上限就是为了保住「事故现场」,逐帧写会把它冲掉。 +`[corner]` 到底在哪一侧,看**打印点**:唯一打印点是 `app/update/appearance.rs`(走 `diag!`), +`platform/win32/corner.rs::format_line` 只拼字符串、不打印 ⇒ 它**进** launcher.log。 -**已知边界(M188 没做,已登记)**:`renderer/**`、`probe.rs`、`platform/win32/corner.rs` 里的行 -仍然只走 stderr——它们各自属于别的 mission 的 scope。要全线接通,得把这些文件的 `eprintln!` -也换成 `crate::app::diag::diag!`(或把 `emit` 提成 lib 级函数)。 +## 3.1 还没接进日志的行(本 mission 之外) + +- `renderer::acceptance/**` 的 `[window]` / `[perf]` / `[caps]`:逐帧/矩阵取证,按上面的判据**故意不接**。 +- `components/**` 的几何探针(`[feedback-probe]` / `[gallery]`):测试通道,不进产品日志。 + +(`renderer/mod.rs` 的 `report_failure`、`renderer/log_bridge.rs`、`renderer/preflight.rs`、 +`probe.rs:apply_with_preflight` 已在 M188 复审后接上。) ## 4. 会话日志搬家:策略是**不搬**