复审 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)
128 lines
5.6 KiB
Rust
128 lines
5.6 KiB
Rust
//! 下载线程与字体注册(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,
|
||
®ister_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))
|
||
}
|