fix(m168): 复审 r2 三项 —— 夹具不再空转、启动不再替英文用户下载、时限不再判慢线下不完
复审 r2(p2-3 / fix-then-merge)三项:
1. **P2-1 爬行服务端是空转的**(`font/tests/support.rs`,从 `tests.rs` 拆出守 400 行):
`seen.windows(4).any(|w| w == b"\n\n")` 拿 4 字节窗口比 2 字节切片 ⇒ `PartialEq` 先比长度 ⇒
恒 false ⇒ 请求头永远读不完、一个字节不发,用例证明的其实只是「连接挂着被掐断」。上一版
这里是 Python 生成时把 `\r\n` 落成了真实换行字节。现在两处都是转义写法
(`b"\r\n\r\n"` + 完整 CRLF 响应头),并且**把「夹具真的在说话」变成断言**:
`serve_trickle()` 返回 `TrickleProof { header, dripped }`,用例断言「真的发过头」「真的挤过
字节」——否则夹具再坏一次也会绿。拆成两条用例:`a_trickling_candidate_is_cut_by_the_attempt_deadline`
([爬行, 正常],300 ms 切断第一个后第二个成功,0.38 s)与
`the_total_deadline_cuts_a_single_crawling_candidate`(单候选 + 400 ms 总预算)。
变异证明:把服务端改成「一个字节都不发」⇒ 两条用例红在「爬行服务端必须真的发出响应头」。
2. **P2-2 启动替英文用户下 17 MiB(真行为 bug)**:
- `fonts::resolve_with` **不再触发下载**(启动那一拍只报告判据)。唯一的触发点收在
`fonts::cjk_for`(`ui/lang.rs::log_cjk`),那一拍的 `setting` 与 `LangChoice` 同源
(ULL_LANG > 配置 > 系统 locale)= 「界面实际用哪种语言」。
- `plan` 把 `is_downloading()` / `Ready` 的闸提到**所有策略之前**:上一版只在 `NeedsDownload`
分支里,"下载中 + Unprobeable" 会把状态改回「无法探测」而线程还在飞,下完又翻回「已就绪」。
当时那条理由(「否则界面停在正在下载」)只对僵死成立 ⇒ worker 现在用 `catch_unwind`
兜底「任何 panic 都写终态」,闸才敢前移。
- 出图通道:新增 `install::set_offline(true)`(`App::with_presenter` 在
`options.screenshot.is_some()` 时调),`plan_with_offline` 在 offline 下永不 `Start`。
于是 `pages/harness/app.rs` 与 `--screenshot` 都不会在 zh-locale 无字体机器上真下 17 MiB。
- 被钉住的旧判据(`Unprobeable + Downloading ⇒ Apply`)改成 `Skip` 并写明旧行为为什么是错的;
新增 `every_strategy_skips_while_a_download_is_in_flight`(四策略打表)与
`the_screenshot_channel_never_starts_a_download`。
3. **P2-3 90 s 单次上限把「慢但能下完」变永久失败**:改 `ATTEMPT_DEADLINE=120 s` /
`TOTAL_DEADLINE=360 s`,并且**最后一个候选拿整个剩余总预算**(前 n-1 个仍 `min(单次, 剩余)`)。
算账写在常量文档里:17 772 300 B / 360 s ≈ 49 KB/s,比 r1 实测最慢那条 IPv6(67~77 KB/s)
还低一档 ⇒ 慢而能下完的线路仍有一次成功机会;健康线路(1.1~16.6 s)在第一/第二个候选就结束。
代价(最坏 6 分钟)也是刻意取舍,写在同一处。
顺手:`i18n::CjkFont::source` 的「尚未钉死」改成指向 `Source::Pinned`;`clock.rs` 与
`general_section.rs` 注释里的 17.0 MB → 16.9 MB。
验证(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-168`,全量重编;取退出码,不截断):
- `cargo test -p unlucky-core --all-targets` → **16 个 target / 1189 passed / 0 failed / exit 0**
(lib 625 + setting 115 + 其余 14 个)
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed / exit 0
- `cargo test -p unlucky-app-iced`(含 `no_hardcoded_tokens` integration)→ **693 passed / 0 failed / exit 0**
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → exit 0
- `cargo fmt --all -- --check` 干净;全仓受检文件 ≤400(本分支最大 361)
This commit is contained in:
1 parent
875b792206
commit
b9009c0e61
11 files changed
+440
-199
No files matched your search
@@ -86,6 +86,10 @@ impl App {
|
||||
// 界面语言(M106):**M92 已经探测对了,问题是一直没人用**——`pages::Model.lang` 此前在
|
||||
// `view::model` 里被写死成 `Lang::En`,于是中文系统上开出来的是全英文界面。
|
||||
let lang = LangState::boot(&bridge, options.screenshot.is_some());
|
||||
// M168(复审 r2 P2-2):出图通道**不许联网**——`log_cjk` 那一拍会走 `after_probe`,
|
||||
// 一台 zh-locale 且没有中文字体的机器上会真的下 17 MiB,并可能在出图过程中注册新字体
|
||||
// (D30:同一份代码要出同一张图)。产品路径不设这个开关。
|
||||
crate::fonts::install::set_offline(options.screenshot.is_some());
|
||||
let mut view = pages::Model::new(
|
||||
pages::Screen::Instances,
|
||||
width,
|
||||
|
||||
@@ -43,7 +43,7 @@ impl App {
|
||||
self.view_dirty = true;
|
||||
}
|
||||
// 中文字体下载(M168):后台线程推进,这里只比对快照。变了就重建视图模型 ⇒ 设置页那一行
|
||||
// 从「正在获取中文字体…(17.0 MB)」翻到「已就绪 / 获取失败」,不需要用户再点一下。
|
||||
// 从「正在获取中文字体…(16.9 MB)」翻到「已就绪 / 获取失败」,不需要用户再点一下。
|
||||
let font_status = crate::fonts::install::status();
|
||||
if font_status != self.font_seen {
|
||||
self.font_seen = font_status;
|
||||
|
||||
@@ -224,8 +224,10 @@ pub fn resolve_with(setting: Option<&str>) -> Resolved {
|
||||
// D28 后半:语言 + 中文环境的中文字体策略。`faces` 的 `None` 在这里是「字体库读不出来」,
|
||||
// 适配层会把它翻成 `Unprobeable` 而不是「缺字体」(否则会误触发一次 CDN 下载)。
|
||||
let cjk_report = cjk::probe_with(setting, faces.as_deref());
|
||||
// M168:只有 `NeedsDownload` 才会在这里发起下载(后台线程),其余三路一次网络都不发。
|
||||
install::after_probe(cjk_report.strategy);
|
||||
// **这里不发起下载**(复审 r2 P2-2 ①):启动那一拍(`boot.rs` / `pages/harness/app.rs`)拿到的
|
||||
// `setting` 是 `None`,只按系统 locale 判——zh-locale + 配置 `en_us` 的机器会在这里就下 17 MiB,
|
||||
// 而随后 `App` 读到配置、把状态翻成「当前界面语言不需要中文字体」,线程却还在下。
|
||||
// 真正的触发点只有一个:[`cjk_for`](`ui/lang.rs::log_cjk`,那一拍已经知道**生效语言**)。
|
||||
|
||||
Resolved {
|
||||
platform,
|
||||
@@ -235,13 +237,18 @@ pub fn resolve_with(setting: Option<&str>) -> Resolved {
|
||||
}
|
||||
}
|
||||
|
||||
/// 语言切换后的中文策略重算(M106 判据 / M168 触发下载):`setting` 是配置里 `ui.language` 的
|
||||
/// **显式值**(`None` = 跟随系统,由 [`crate::platform::locale::SystemLocale`] 决定)。
|
||||
/// 语言切换后的中文策略重算(M106 判据)+ **唯一**的下载触发点(M168):`setting` 是配置里
|
||||
/// `ui.language` 的**显式值**(`None` = 跟随系统,由 [`crate::platform::locale::SystemLocale`] 决定)。
|
||||
///
|
||||
/// 为什么需要它:启动那一轮([`resolve`])拿不到配置,只按系统 locale 判。用户在英文系统上
|
||||
/// 手选「简体中文」时,若不复算,策略会停在 `NotNeeded`——界面文字是中文、字体却没有中文字形,
|
||||
/// 那就是白块。复算出的 `NeedsDownload` 现在**真的会去下载**(M168:`install::after_probe`),
|
||||
/// 结果进设置页那一行(`pages::settings::general`)与 stderr 的 `[font] cjk-download …` 日志。
|
||||
/// 那就是白块。
|
||||
///
|
||||
/// **为什么触发只留在这里**(复审 r2 P2-2 ①):这一拍的 `setting` 与 `LangChoice` 同源
|
||||
/// (`ULL_LANG` > 配置 > 系统 locale),也就是「界面实际用哪种语言」的那一份判据。
|
||||
/// 放在 [`resolve`] 里就会用「系统 locale」代表「用户选择」,zh-locale + 配置 `en_us` 的机器
|
||||
/// 会白下一次 17 MiB。出图通道(`--screenshot`)由 `install::set_offline(true)` 兜住,
|
||||
/// 仍然一次都不发。
|
||||
pub fn cjk_for(setting: Option<&str>) -> CjkReport {
|
||||
let report = cjk::probe_with(setting, system_faces().as_deref());
|
||||
install::after_probe(report.strategy);
|
||||
|
||||
@@ -250,34 +250,54 @@ pub enum Step {
|
||||
|
||||
/// 判据表(判据本体就在这张表的 `match` 里,`after_probe` 不含任何判据)。
|
||||
///
|
||||
/// | 策略 | 当前状态 | 这一步 |
|
||||
/// | 当前状态 | 策略 | 这一步 |
|
||||
/// |---|---|---|
|
||||
/// | `NotNeeded` / `System` / `Unprobeable` | 任意 | `Apply`(零网络) |
|
||||
/// | `NeedsDownload` | 下载中 / 已就绪 | **`Skip`**(幂等:切一次语言不起第二条线程) |
|
||||
/// | `NeedsDownload` | 未判定 / 失败过 / 其它 | `Start`(失败后的重试只能是用户再切一次语言) |
|
||||
/// | **下载中**(任意策略) | 任意 | **`Skip`**(闸在最上面,见下) |
|
||||
/// | `Ready` | 任意 | `Skip`(幂等:已就绪不再发起) |
|
||||
/// | 其它 | `NotNeeded` / `System` / `Unprobeable` | `Apply`(只改状态,零网络) |
|
||||
/// | 其它 | `NeedsDownload` | `Start`(失败过的重试只能由用户再切一次语言触发) |
|
||||
///
|
||||
/// ## 为什么「下载中」要在**所有**策略之前先跳过(复审 r2 P2-2 ②)
|
||||
///
|
||||
/// 上一版把 `is_downloading()` 只放在 `NeedsDownload` 分支里,于是「下载中 + `Unprobeable`」
|
||||
/// 会走 `Apply(Unprobeable)` —— 把状态从「正在下载」改回「无法探测」,而**线程还在飞**:
|
||||
/// 下完之后 `Ready` 又把它盖回去,状态行在 30 秒里来回翻。当时给出的理由是「否则界面会停在
|
||||
/// 正在下载」——那个理由只对**僵死**的 `Downloading` 成立(线程 panic 才会僵死),而在飞是
|
||||
/// 常见情形。僵死那一半现在由 worker 的 `catch_unwind` 兜底(任何 panic 都会写终态),
|
||||
/// 所以「下载中一律 Skip」不再有卡死的风险。
|
||||
///
|
||||
/// `offline = true`(出图通道)时**永不** `Start`:D30 要求同一份代码出同一张图,而下载会额外
|
||||
/// 产生网络流量并可能在出图过程中注册新字体。
|
||||
#[must_use]
|
||||
pub fn plan(strategy: Strategy, current: &CjkInstall) -> Step {
|
||||
pub fn plan_with_offline(strategy: Strategy, current: &CjkInstall, offline: bool) -> Step {
|
||||
if current.is_downloading() || matches!(current, CjkInstall::Ready { .. }) {
|
||||
return Step::Skip;
|
||||
}
|
||||
match strategy {
|
||||
Strategy::NotNeeded => Step::Apply(CjkInstall::NotNeeded),
|
||||
Strategy::System { family } => Step::Apply(CjkInstall::System {
|
||||
family: family.to_owned(),
|
||||
}),
|
||||
Strategy::Unprobeable => Step::Apply(CjkInstall::Unprobeable),
|
||||
Strategy::NeedsDownload(font) => {
|
||||
if current.is_downloading() || matches!(current, CjkInstall::Ready { .. }) {
|
||||
Step::Skip
|
||||
} else {
|
||||
Step::Start(font)
|
||||
}
|
||||
}
|
||||
// 出图通道需要下载 ⇒ **什么都不做**(也不改状态):写「不需要下载」是谎话,
|
||||
// 写「正在下载」也是谎话(并没有在下载)。
|
||||
Strategy::NeedsDownload(_) if offline => Step::Skip,
|
||||
Strategy::NeedsDownload(font) => Step::Start(font),
|
||||
}
|
||||
}
|
||||
|
||||
/// 同 [`plan_with_offline`],但按本进程当前的联网策略(生产判据入口)。
|
||||
#[must_use]
|
||||
pub fn plan(strategy: Strategy, current: &CjkInstall) -> Step {
|
||||
plan_with_offline(strategy, current, offline())
|
||||
}
|
||||
|
||||
/// 判据结论 → 现在这一步的结局(生产入口;判据全在 [`plan`],这里只执行)。
|
||||
///
|
||||
/// * 英文环境 / 系统已有 / 探测不出 → 只更新状态(零网络);
|
||||
/// * 已经在下载 / 已就绪 → 什么都不做(幂等:切一次语言不会起第二条下载、不会重复注册 17 MiB);
|
||||
/// * 上次失败过 → 只有再次切语言才会重试(用户显式动作),不会自己反复联网。
|
||||
/// * 英文环境 / 系统已有 / 探测不出 → 只更新状态(零网络);
|
||||
/// * 上次失败过 → 只有再次切语言才会重试(用户显式动作),不会自己反复联网;
|
||||
/// * 出图通道([`set_offline`])→ 永不发起。
|
||||
pub fn after_probe(strategy: Strategy) -> CjkInstall {
|
||||
match plan(strategy, &status()) {
|
||||
Step::Skip => status(),
|
||||
@@ -292,6 +312,24 @@ pub fn after_probe(strategy: Strategy) -> CjkInstall {
|
||||
}
|
||||
}
|
||||
|
||||
/// 本进程是否**禁止**发起字体下载(出图通道用;D30:同一份代码出同一张图)。
|
||||
///
|
||||
/// 为什么需要它:截图/演示通道也会走 `App` 构造 → `lang.log_cjk()` → `cjk_for` → `after_probe`。
|
||||
/// 一台 zh-locale、没有中文字体的机器上,出图会真的下 17 MiB 并在出图过程中注册新字体——
|
||||
/// 这既产生流量,也让「同一份代码出同一张图」不再成立。
|
||||
pub fn set_offline(offline: bool) {
|
||||
OFFLINE.store(offline, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// 当前是否处于 [`set_offline`] 的状态。
|
||||
#[must_use]
|
||||
pub fn offline() -> bool {
|
||||
OFFLINE.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 进程级联网开关(默认允许)。
|
||||
static OFFLINE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// 状态变更时记一行(只有真变了才打,避免每秒刷一行)。
|
||||
pub(super) fn update(state: CjkInstall) {
|
||||
let previous = {
|
||||
|
||||
@@ -218,13 +218,78 @@ fn the_idle_strategies_only_apply_state() {
|
||||
family: "SimSun".to_owned()
|
||||
})
|
||||
);
|
||||
// 复审 r2 P2-2 ② 改判:**下载中一律 Skip**,即使策略说「探测不出」。
|
||||
// 上一版在这里断言 `Apply(Unprobeable)`,理由是「否则界面会停在正在下载」——那个理由只对
|
||||
// 僵死成立(线程 panic),而在飞是常见情形:线程还在下,状态却被改成「无法探测」,下完又
|
||||
// 翻回「已就绪」,状态行在几十秒里来回翻。僵死由 worker 的 catch_unwind 兜底。
|
||||
assert_eq!(
|
||||
plan(Strategy::Unprobeable, &CjkInstall::Downloading { bytes: 1 }),
|
||||
Step::Apply(CjkInstall::Unprobeable),
|
||||
"探测不出也要把状态改回来(否则界面会停在「正在下载」)"
|
||||
Step::Skip,
|
||||
"在飞的下载不许被别的策略改写状态"
|
||||
);
|
||||
}
|
||||
|
||||
/// **下载中一律 `Skip`**(复审 r2 P2-2 ②):四种策略挨个打一遍。
|
||||
///
|
||||
/// 抓得住:把 `is_downloading()` 那道闸挪回 `NeedsDownload` 分支里(上一版就是这样)——
|
||||
/// `Unprobeable` / `NotNeeded` / `System` 三行立刻红。
|
||||
/// 抓不住:worker 真的永不僵死(那要制造 panic,见 `worker` 的 `catch_unwind` 与报告的「没验」)。
|
||||
#[test]
|
||||
fn every_strategy_skips_while_a_download_is_in_flight() {
|
||||
let downloading = CjkInstall::Downloading { bytes: 17_772_300 };
|
||||
for strategy in [
|
||||
Strategy::NeedsDownload(&NOTO_SANS_SC),
|
||||
Strategy::NotNeeded,
|
||||
Strategy::System { family: "SimSun" },
|
||||
Strategy::Unprobeable,
|
||||
] {
|
||||
assert_eq!(
|
||||
plan(strategy, &downloading),
|
||||
Step::Skip,
|
||||
"{strategy:?}:下载中不许改状态、不许起第二条线程"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 出图通道(`offline`)**永不发起下载**(D30:同一份代码出同一张图)。
|
||||
///
|
||||
/// 抓得住:删掉 `plan_with_offline` 里 `NeedsDownload(_) if offline => Skip` 那一臂 ⇒ 截图/演示
|
||||
/// 通道会在 zh-locale 且无中文字体的机器上真下 17 MiB,用例在 `Step::Skip` 处红。
|
||||
/// 抓不住:真出图时有没有产生流量(那要真机跑 `--screenshot`,见报告「没验」)。
|
||||
#[test]
|
||||
fn the_screenshot_channel_never_starts_a_download() {
|
||||
assert_eq!(
|
||||
plan_with_offline(
|
||||
Strategy::NeedsDownload(&NOTO_SANS_SC),
|
||||
&CjkInstall::Unknown,
|
||||
true
|
||||
),
|
||||
Step::Skip,
|
||||
"出图通道不许联网"
|
||||
);
|
||||
assert_eq!(
|
||||
plan_with_offline(
|
||||
Strategy::NeedsDownload(&NOTO_SANS_SC),
|
||||
&CjkInstall::Unknown,
|
||||
false
|
||||
),
|
||||
Step::Start(&NOTO_SANS_SC),
|
||||
"正常通道该发起(否则上面那条可能是「永远 Skip」的假绿)"
|
||||
);
|
||||
// 只改状态的三种策略在两条通道里表现一致(它们本来就不联网)。
|
||||
for strategy in [
|
||||
Strategy::NotNeeded,
|
||||
Strategy::System { family: "SimSun" },
|
||||
Strategy::Unprobeable,
|
||||
] {
|
||||
assert_eq!(
|
||||
plan_with_offline(strategy, &CjkInstall::Unknown, false),
|
||||
plan_with_offline(strategy, &CjkInstall::Unknown, true),
|
||||
"{strategy:?}:不联网的策略与通道无关"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 截图钉子(D30):每个 `ULL_FONT_STATUS` 取值都能解析成一档状态,坏值不猜。
|
||||
///
|
||||
/// 抓得住:出图夹具写了一个不存在的取值却静默出「未知」状态的图(报告就会撒谎)。
|
||||
|
||||
@@ -68,11 +68,23 @@ pub(super) fn start(font: &'static font::CjkFont) {
|
||||
|
||||
// 判据说「需要」,这条线程才存在:不需要的三种情形在 `after_probe` 就返回了。
|
||||
std::thread::spawn(move || {
|
||||
let state = ensure_for(
|
||||
Strategy::NeedsDownload(font),
|
||||
&fetcher,
|
||||
®ister_into_font_system,
|
||||
);
|
||||
// **保证写终态**(复审 r2 P2-2 ② 把这条当成「下载中一律 Skip」的前提):
|
||||
// 线程里任何 panic(库内部、注册路径、OOM 前的 unwrap)都要落成 `Failed`,
|
||||
// 否则状态会永远停在「正在获取中文字体…」而幂等闸又不许别人改它。
|
||||
let state = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
ensure_for(
|
||||
Strategy::NeedsDownload(font),
|
||||
&fetcher,
|
||||
®ister_into_font_system,
|
||||
)
|
||||
})) {
|
||||
Ok(state) => state,
|
||||
Err(_) => CjkInstall::Failed {
|
||||
class: FetchClass::Disk,
|
||||
message: "字体安装线程 panic(已捕获,状态回到终态;界面继续用系统字体)"
|
||||
.to_owned(),
|
||||
},
|
||||
};
|
||||
// `from_cache` 只认下载器**这次**的结论(复审 r1 次要项):取之前「文件在不在」会说谎——
|
||||
// 坏缓存会被删掉重下,那时 `has_cached()` 还是 true,而真相是「联网下了一次」。
|
||||
let from_cache = fetcher.was_cached().unwrap_or(false);
|
||||
|
||||
@@ -102,7 +102,7 @@ fn the_font_status_row_speaks_in_four_states_and_two_languages() {
|
||||
assert!(value.contains(reason), "失败原因没进文案:{value}");
|
||||
}
|
||||
|
||||
// 下载中写的是**真实**字节数(17.0 MB),不是假百分比。
|
||||
// 下载中写的是**真实**字节数(17 772 300 B → 16.9 MiB),不是假百分比。
|
||||
let (value, _) = general::font_status(&zh, &CjkInstall::Downloading { bytes: 17_772_300 });
|
||||
assert!(
|
||||
value.contains("16.9 MB"),
|
||||
|
||||
Reference in new issue
Block a user