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

+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)