Files
Unlucky-Launcher/crates/app-iced/src/fonts/install/worker.rs
T
AskaEth 875b792206 fix(m168): 复审 r1 五项 —— 修红的 integration target、独立失败域、整体时限
复审 r1(p1-1/fix-then-merge)五项全改:

1. **p1-1 CI 会红的 4 条 integration test**(`crates/core/tests/setting/`,我上一轮只跑了 `--lib`,
   scope 漏掉了覆盖 schema 版本号的目录式 target):
   - `java_install_keys` / `isolation_keys` / `appearance_keys`:版本断言 1.6.0 → 1.7.0(M168 的
     `ui.font_cdn`),并写明「每加一批键推一次 minor 是纪律」。
   - `update_keys::a_read_only_launcher_config_never_retires_the_legacy_file`:夹具从**写死
     1.7.0** 改成 `launcher_version().minor + 1` 经 `SchemaRef::url()` 生成。写死数字的版本一推
     这条守卫就变成「同版本」⇒ 文件可写 ⇒ 迁移真的执行 ⇒ 旧文件被退役,而断言还绿着(用户数据
     守卫静默失效)。相对推导不会过期。
2. **p2-1 备用镜像不备用**:`cdn` 与 `fastly` 同属 Fastly(都 CNAME 到 jsdelivr.map.fastly.net),
   换掉。现在首选 `gcore.jsdelivr.net`(Cloudflare,独立失败域),二选 `cdn.jsdelivr.net`
   (Fastly)。注释里放了实测表(本机 + 复审机两份数据、字节数、sha256、IPv6 慢路径),
   并写清排序依据(Fastly 侧 IPv6 实测 67 KB/s ⇒ 丢掉 4 分钟;Cloudflare 侧两次都完整下完)。
3. **p2-2 整体时限**:`core::download::font` 新增 `ATTEMPT_DEADLINE = 90 s` /
   `TOTAL_DEADLINE = 180 s`,落在本模块自己的候选循环里(不动引擎全体语义):每次尝试
   `min(单次上限, 剩余总预算)`,超时归 `Network` 档。新增守卫
   `a_trickling_connection_is_cut_by_the_deadline`(回环服务端每 100 ms 挤 1 字节、预算 300 ms)。
   顺带修掉一个真 bug:`tokio::time::timeout` 在运行时外构造会 panic(5 条既有用例当场抓到),
   现在整段包进 `block_on(async { … })`。UI 文案改成「网络不通或太慢(超时)」,不校验失败混。
4. **p2-3 说 `Source::PendingPin` 的两处**:`fonts/cjk.rs` 的模块文档已改;`ui/lang.rs::log_cjk`
   那句「D28 的下载通道还是 PendingPin」(在下载已发起之后打印,等于当面否定本功能)改成
   「已按 D28 发起下载,见 [font] cjk-download … 与设置页那一行」。全仓再扫,只剩注释里的历史说明。
5. **p2-4 假守卫**:判据抽成纯函数 `install::plan(strategy, current) -> Step`(`Skip` / `Apply` /
   `Start`),`after_probe` 只执行。旧的空转用例换成打真判据的打表用例;变异证明:删掉
   `Skip` 那一臂 ⇒ `a_started_or_finished_install_is_not_restarted` 红(已实跑)。

次要项也一并处理:`from_cache` 改成**本次是否真的命中缓存**(`CdnFetcher::was_cached()`,
`has_cached()` 降级为诊断字段 `cache_present=`);`FontSpec` 的 URL 口径收紧为「非回环必须
https」(`is_acceptable_url` + 守卫);`ui.font_cdn` 写进状态行 hint;文档里的 17.0 MB → 16.9 MB。
`download/font.rs` 拆出 `font/spec.rs` 守 400 行。

验证(这台机器上单个 800 MB 测试 exe 链接要 10 分钟,链接期间文件被 Defender 扫描,故耗时很长):
- `cargo test -p unlucky-core --all-targets` → **1188 passed / 0 failed**(16 个 target,含
  `setting` 那个 115 条、上一轮红 4 条的 integration target)
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed
- `cargo test -p unlucky-app-iced --lib` → 685 passed / 0 failed
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → 干净
- `cargo fmt --all -- --check` → 干净;全仓受检文件行数最大 388(<400)
2026-11-08 14:38:23 +08:00

128 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 下载线程与字体注册(M168):`install.rs` 的「动作」那一半。
//!
//! ## 为什么拆出来
//!
//! `install.rs` 是「状态 + 判据 + 触发」,已经贴着 400 行硬线。这里只放真正干活的两件:
//!
//! 1. [`start`]:起一条后台线程(进程内同时只有一条),把「正在下载 → 结果」写回状态槽;
//! 2. [`register_into_font_system`]:把字节交给 iced 的全局字体系统(运行时注册)。
//!
//! ## 为什么下载在后台线程
//!
//! 一份 17 MiB 的字体在弱网下要几十秒,而两个触发点(启动编排、设置页切语言)都在 UI 线程上。
//! 在 UI 线程同步下完 = 窗口几十秒不响应(启动路径甚至是「窗口还没出来」)。
//!
//! ## 线程里做了什么(顺序即判据)
//!
//! ```text
//! CdnFetcher(缓存命中 ⇒ 零网络;否则共享引擎下载 + sha256 复验)
//! → i18n::font::ensure 只有 NeedsDownload 才调 fetcher
//! → 成功:注册进 iced,状态 Ready{family, from_cache}
//! → 失败:状态 Failed{class},**什么都不注册**(降级回系统字体,界面照常可用)
//! ```
//!
//! ## 诚实边界
//!
//! - **抓得住**:状态流转、失败不注册、日志三行(start / ok / failed);
//! - **抓不住**:真机上字体注册之后**出字的样子**(cosmic-text 用 `wght` 轴选字重这件事只核对了
//! 上游源码,没有真机截图对照;macOS / Linux 完全没有真机)。
use unlucky_i18n::font::{self, Strategy};
use super::{ensure_for, update, CjkInstall, FetchClass};
use crate::fonts::cdn::CdnFetcher;
use crate::platform::data;
/// 起一条后台下载(进程内同时只允许一条:`install::after_probe` 负责那道闸)。
pub(super) fn start(font: &'static font::CjkFont) {
let font::Source::Pinned { bytes, .. } = font.source;
let Some(cache_root) = data::cache_root() else {
update(CjkInstall::Failed {
class: FetchClass::Disk,
message: "数据根解析不出来,字体无处可落".to_owned(),
});
return;
};
let fetcher = match CdnFetcher::new(font, cache_root) {
Ok(fetcher) => fetcher,
Err(error) => {
update(CjkInstall::Failed {
class: FetchClass::from(&error),
message: error.message().to_owned(),
});
return;
}
};
let cache_file = fetcher.cache_file();
update(CjkInstall::Downloading { bytes });
eprintln!(
"[font] cjk-download start family=\"{}\" bytes={bytes} file={} cache_present={}",
font.family,
cache_file.display(),
// 这条只是「文件在不在」(诊断用),**不是** from_cache:坏缓存会被删掉重下,
// 真正的命中结论在 `fetcher.was_cached()`(取完之后)。
fetcher.has_cached()
);
// 判据说「需要」,这条线程才存在:不需要的三种情形在 `after_probe` 就返回了。
std::thread::spawn(move || {
let state = ensure_for(
Strategy::NeedsDownload(font),
&fetcher,
&register_into_font_system,
);
// `from_cache` 只认下载器**这次**的结论(复审 r1 次要项):取之前「文件在不在」会说谎——
// 坏缓存会被删掉重下,那时 `has_cached()` 还是 true,而真相是「联网下了一次」。
let from_cache = fetcher.was_cached().unwrap_or(false);
let state = match state {
CjkInstall::Ready { family, .. } => CjkInstall::Ready { family, from_cache },
other => other,
};
match &state {
CjkInstall::Ready { family, from_cache } => eprintln!(
"[font] cjk-download ok family=\"{family}\" from_cache={from_cache} (中文字形现在由它兜底,界面不需要重启)"
),
CjkInstall::Failed { class, message } => eprintln!(
"[font] cjk-download failed class={} {message}(降级:继续用系统字体,界面照常可用)",
class.as_str()
),
other => eprintln!("[font] cjk-download {}", other.describe()),
}
update(state);
});
}
/// 把字体字节注册进 iced 的**全局字体系统**(`iced_graphics` 的 `FontSystem::load_font`)。
///
/// 返回注册后的族名(从字体库里读回来的):变量字体的 `name` 表里族名是 `Noto Sans SC`、
/// 而默认实例是 Thin,所以**不能**假设「注册了就叫 NOTO_SANS_SC.family」——这里读回来核对。
///
/// # Errors
/// 字体系统锁被毒掉、或注册后字体库里找不到那个族(字节不是字体 / `fontdb` 不认)。
pub fn register_into_font_system(bytes: Vec<u8>) -> Result<String, String> {
let lock = iced::advanced::graphics::text::font_system();
let mut font_system = lock
.write()
.map_err(|_| "字体系统的读写锁被毒掉(另一处注册 panic 过)".to_owned())?;
font_system.load_font(std::borrow::Cow::Owned(bytes));
// 先把族名抄出来再判:`faces()` 的迭代器借着 `font_system`,直接链到 `ok_or_else` 会让
// 那个借用活过守卫(E0597)。
let families: Vec<String> = font_system
.raw()
.db()
.faces()
.flat_map(|face| face.families.iter())
.map(|(family, _)| family.clone())
.collect();
families
.iter()
.find(|family| family.as_str() == font::NOTO_SANS_SC.family)
.cloned()
.ok_or_else(|| format!("注册后在字体库里找不到 \"{}\"", font::NOTO_SANS_SC.family))
}