fix(m188): r1 复审 P2-1——后端启动失败的具名原因进 launcher.log

复审结论 p2-1/1 item + fix-then-merge:主线成立,但「启动器自己因 GPU / 渲染后端起不来」的
事故原因仍只进 stderr —— 双击启动没有 stderr,那条路径上等于没有日志。

- renderer/mod.rs::report_failure 的两行(`[gpu] <backend> 初始化失败(具名)` /
  `[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 行补上「渲染后端起不来的具名原因」。
This commit is contained in:
worker-logplace committed 2026-11-09 01:05:40 +08:00
1 parent 5fd5cd47dc
commit 8c5bebf3e3
9 files changed
+87 -27

No files matched your search

+1 -1
View File
@@ -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/<session-id>.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. |
+1 -1
View File
@@ -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/<session-id>.jsonl` | 每次启动一份 JSONL 事件流:命令行(已脱敏)、每一行 stdout/stderr、退出码。 |
| `logs/game/index.jsonl` | 一会话一行摘要的索引,日志页左栏读的就是它。 |
+8 -6
View File
@@ -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] <backend> 初始化失败(具名)` / `[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";
+1 -1
View File
@@ -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;
+6 -3
View File
@@ -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"
);
+3 -1
View File
@@ -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) {}
+45 -2
View File
@@ -199,9 +199,12 @@ pub fn forced_fallback() -> Option<Kind> {
/// `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();
}
}
+6 -2
View File
@@ -76,10 +76,14 @@ pub fn run_if_requested() -> Option<Result<Verdict, Failure>> {
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)
+16 -10
View File
@@ -41,20 +41,26 @@ GUI 的 `--data-dir <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] <backend> 初始化失败(具名)` 与 `[gpu] 失败详情(具名)`(`renderer::report_failure`,复审 P2-1)、`[gpu] <target>: …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. 会话日志搬家:策略是**不搬**