CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Release / 选槽位(默认两个 Windows) (push) Canceled after 0s
Release / build (${{ matrix.slot }}) (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s
118 lines
5.8 KiB
Rust
118 lines
5.8 KiB
Rust
//! 起步后端失败后的**兜底/重开**一簇:判据、进程环境交接、`respawn`、退出码透传。
|
||
//!
|
||
//! 从 `app/boot.rs` 拆出来的理由:这一簇**与启动编排零耦合**,但已有 5 条测试,而且它是
|
||
//! M97 判据表第 8/9 行的落点。`boot.rs` 那边只负责「什么时候调它」。
|
||
//!
|
||
//! ## 为什么必须换进程(别改回进程内重试)
|
||
//!
|
||
//! Windows 实测本进程里再 `run()` 一次直接 panic
|
||
//! (`iced_winit-0.14.1/src/lib.rs:81` → `Create event loop: RecreationAttempt`),winit 一个进程
|
||
//! 只能建一次事件循环。
|
||
|
||
use crate::renderer::Decision;
|
||
use crate::renderer::{self, acceptance, Kind};
|
||
|
||
/// 起步后端失败后的去向(判据表第 8 行 vs 第 9 行——**两条路策略不同,别合成一条**)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub(super) enum OnFailure {
|
||
/// 交互启动:安全网优先,换进程用这个后端重开。
|
||
Respawn(Kind),
|
||
/// 取证启动:红着退出,**不出图**。
|
||
Refuse,
|
||
}
|
||
|
||
/// 纯判据:起步后端失败时走哪条路。
|
||
///
|
||
/// `evidence` 非空 = 本次是取证启动(带 `--screenshot`);返回 `None` = 没有兜底可用
|
||
/// (`--renderer=cpu` / 未编 wgpu),原样把错误交回去。抽成纯函数只为一个理由:这条分支以前
|
||
/// **没有任何测试**,`respawn` / `Refuse` 两条路可以被静默改反(M97 r1 p2-1 就是这么测出来的)。
|
||
pub(super) fn on_failure(
|
||
decision: Decision,
|
||
evidence: Option<&std::path::PathBuf>,
|
||
) -> Option<OnFailure> {
|
||
let fallback = decision.fallback?;
|
||
Some(match evidence {
|
||
Some(_) => OnFailure::Refuse,
|
||
None => OnFailure::Respawn(fallback),
|
||
})
|
||
}
|
||
|
||
/// 降级重开要传给子进程的环境变量(**两条缺一不可**)。
|
||
///
|
||
/// ① `ICED_BACKEND`:让子进程直接用 `fallback` 跑。
|
||
/// ② [`renderer::FALLBACK_ENV`]:**防无限重开的闸门**——子进程见它就必须跳过探测
|
||
/// (`boot_probe` 的第一个分支),否则会算出同一个结论、再失败、再 respawn。
|
||
///
|
||
/// 抽成纯函数是为了让「漏传其中之一」变成**测试能抓到**的错误:两条都是静默失效型
|
||
/// (漏 ① 重开又跑一遍 wgpu、漏 ② 无限重开),线上极难看出来。
|
||
pub(super) fn fallback_env(fallback: Kind) -> [(&'static str, &'static str); 2] {
|
||
[
|
||
("ICED_BACKEND", fallback.env_value()),
|
||
(renderer::FALLBACK_ENV, fallback.env_value()),
|
||
]
|
||
}
|
||
|
||
/// 安全网:起一个**新进程**用 `fallback` 后端重开,然后带着它的退出码退出。
|
||
///
|
||
/// 只在**交互启动**时走到这里(取证启动宁可红着退出也不换后端出图);子进程看到
|
||
/// [`renderer::FALLBACK_ENV`] 必须跳过探测,否则无限递归重开(闸门在 `boot_probe` 入口)。
|
||
pub(super) fn respawn(failed: Kind, fallback: Kind, error: &str) -> ! {
|
||
let exe = std::env::current_exe().unwrap_or_else(|err| {
|
||
eprintln!("[gpu] 降级重开失败:拿不到自身路径({err})");
|
||
std::process::exit(exit_code(None));
|
||
});
|
||
eprintln!(
|
||
"[gpu] 降级 → {}({} 已失败,原因见上两行;换一个进程重开:winit 不允许同进程建两次事件循环)。\
|
||
下一次的日志从 `[probe] 本次是降级重开` 开始。",
|
||
fallback.name(),
|
||
failed.name()
|
||
);
|
||
// `[backend] final` 一行把「哪个后端失败了、去了哪、为什么」收在一起(可 grep 的单一结论行)。
|
||
// 这里 `final=` 装的是**兜底**(进程即将带着它重开);`via=respawn` 说明它是换进程去的。
|
||
acceptance::report_backend_failed(failed, Some(fallback), Some(fallback), "respawn", error);
|
||
|
||
let status = fallback_command(&exe, std::env::args_os().skip(1), fallback).status();
|
||
|
||
// 退出码**透传**子进程的:上层脚本/CI 靠它判断成败。写错的失效方式是「安静地给出一个错
|
||
// 结论」——不崩、不静默降级、不无限重开,只是让 CI 把失败当成功。
|
||
let code = match status {
|
||
Ok(status) => exit_code(status.code()),
|
||
Err(err) => {
|
||
eprintln!("[gpu] 降级重开失败:{err}");
|
||
exit_code(None)
|
||
}
|
||
};
|
||
std::process::exit(code)
|
||
}
|
||
|
||
/// 子进程退出码 → 本进程退出码。
|
||
///
|
||
/// `None` = 子进程被信号杀死(只有 Unix):走兜底码而**不是** panic、也不是 0——上层判据是
|
||
/// 「非零 = 失败」,「拿不到退出码」和「退出码是某个非零值」对脚本是一回事。
|
||
pub(super) fn exit_code(code: Option<i32>) -> i32 {
|
||
/// 重开失败 / 拿不到退出码时的兜底码(与 `main` 的「参数或运行错误」一致)。
|
||
const RESPAWN_FAILED: i32 = 2;
|
||
code.unwrap_or(RESPAWN_FAILED)
|
||
}
|
||
|
||
/// 构造降级重开的子进程命令(**可测**:program / argv / env 都能在测试里断言)。
|
||
///
|
||
/// `program` / `args` 是参数而不是在函数里读 `std::env`,唯一目的是让测试能喂假命令
|
||
/// (`cmd /c exit 7` / `sh -c "exit 7"`)来验退出码透传——真用 `current_exe()` 就没法测。
|
||
///
|
||
/// 三路 stdio **显式**继承:子进程日志必须和父进程同一路输出,否则验收只抓到父进程那两行具名
|
||
/// 错误。`Command` 没有 stdio 的 getter(stable 没有),所以这条只能靠真跑一次证明。
|
||
pub(super) fn fallback_command(
|
||
program: &std::path::Path,
|
||
args: impl IntoIterator<Item = std::ffi::OsString>,
|
||
fallback: Kind,
|
||
) -> std::process::Command {
|
||
let mut command = std::process::Command::new(program);
|
||
command.args(args).envs(fallback_env(fallback));
|
||
command
|
||
.stdin(std::process::Stdio::inherit())
|
||
.stdout(std::process::Stdio::inherit())
|
||
.stderr(std::process::Stdio::inherit());
|
||
command
|
||
}
|