M205: gh-proxy 键绑定断言、外层重试预算判据、UA 版本号来源、下载设置文档更正
- setting/keys/proxy: 新增断言把 KEY_GH_PROXY 常量与 schema 登记绑起来(键名 / Text{max_len:256} / 默认空串)
- addon/online/download: RETRY_ATTEMPTS 与引擎每源次数是两件事(层级相乘、管的失败类型不同),写清判据并用两条编译期断言钉住 3 与 3x3=9 的边界
- presenter/host/addon/http: UA 版本号改为从 CARGO_PKG_VERSION 拼出,补断言钉住版本 == CARGO_PKG_VERSION 且联系方式段不丢
- docs/download-settings: §3.1 四条 CLI 命令都吃设置(update 是自更新路径),§6 补 M11-M13 三条 CLI 变异,§1/§7 里 gh_proxy 已合入的事实
This commit is contained in:
1 parent
4c29cbac3e
commit
d4aa3b23f4
4 files changed
+119
-16
No files matched your search
@@ -25,14 +25,45 @@ use std::path::{Path, PathBuf};
|
||||
use sha1::Digest as _;
|
||||
use sha2::Sha512;
|
||||
|
||||
use crate::download::config::DEFAULT_MAX_ATTEMPTS_PER_SOURCE;
|
||||
use crate::download::{DownloadEngine, DownloadRequest, Integrity, ResourceKind};
|
||||
|
||||
use super::modrinth::HashAlgorithm;
|
||||
use super::transport::{OnlineError, OnlineErrorKind, OnlineFuture};
|
||||
|
||||
/// 最多尝试几次(含首次)。3 次 = 镜像/健康度换源之外还给一次真重试。
|
||||
/// 本模块外层「整条下载」的预算(含首次):最多取几遍文件。
|
||||
///
|
||||
/// # 它**不是**引擎的每源尝试次数,所以不引用 `download::config` 的常量(M205 判定)
|
||||
///
|
||||
/// 数值恰好都是 3,但数的不是同一件事。三条判据,任一条都足够说明「合并成一处」会把语义拍平:
|
||||
///
|
||||
/// 1. **层级不同,而且是相乘**:引擎那层在 [`ArtifactFetcher`] 里面,按**源**数(镜像展开后每个候选
|
||||
/// 各有一份预算,见 `DownloadEngine::attempts_for`;源在冷却期时还会缩成 1 次恢复探测)。本模块这
|
||||
/// 层只数「取一遍 + 复核一遍」,每数一次都要重新走一遍引擎 —— 最坏情况下的真实请求数 ≈
|
||||
/// [`RETRY_ATTEMPTS`] × 引擎每源预算,合并成一个数等于把最坏请求数变成平方级。
|
||||
/// 2. **管的失败类型不同**:sha512 不符与字节数不符只有本模块知道(引擎只认 sha1,`Integrity` 没有
|
||||
/// Sha512 档),这类失败引擎**不会**重试,必须由这一层重取 —— 这一层的预算里有一档是引擎看不见的。
|
||||
/// 3. **改它的性质不同**:`download.retry_limit` 是用户可见的设置(0~10、进 JSON schema,也就是说引擎
|
||||
/// 那个默认值会因为**用户的**配置而变);这里的 3 是内容库元数据请求的产品口径,没有设置键,也不该
|
||||
/// 被某个设置项的出厂默认牵着走。
|
||||
///
|
||||
/// **边界**:最坏情况 = [`RETRY_ATTEMPTS`](外层)× [`DEFAULT_MAX_ATTEMPTS_PER_SOURCE`](引擎每个源
|
||||
/// 一次请求内的尝试)= 9 次真实网络请求。下面两条注解级断言(编译期求值)钉住这个乘积:任一侧想改,
|
||||
/// 都得先读到这段判据,而不是被一次「统一常量」的顺带改动悄悄翻倍。
|
||||
pub const RETRY_ATTEMPTS: u32 = 3;
|
||||
|
||||
/// 外层预算被改了吗(改之前先读 [`RETRY_ATTEMPTS`] 的 doc:它不是引擎的每源次数)。
|
||||
const _: () = assert!(
|
||||
RETRY_ATTEMPTS == 3,
|
||||
"外层下载预算变了:它不是 download::config 的每源尝试次数,改它要单独一个决定"
|
||||
);
|
||||
|
||||
/// 最坏情况下的真实请求数(3 × 3)——外层与引擎每源这两个「3」不共享来源,乘积才是事实。
|
||||
const _: () = assert!(
|
||||
RETRY_ATTEMPTS as u64 * DEFAULT_MAX_ATTEMPTS_PER_SOURCE as u64 == 9,
|
||||
"最坏真实请求数变了(外层 × 引擎每源):先读 RETRY_ATTEMPTS 的 doc 再改"
|
||||
);
|
||||
|
||||
/// 一次下载请求(只含「引擎需要什么」,不含任何站点字段)。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FetchArgs<'a> {
|
||||
|
||||
@@ -74,4 +74,23 @@ mod tests {
|
||||
assert!(gh.doc.contains("留空 = 跟随内置默认"), "{}", gh.doc);
|
||||
assert!(gh.doc.contains("填 off 关闭"), "{}", gh.doc);
|
||||
}
|
||||
|
||||
/// **`KEY_GH_PROXY` 与 schema 登记的绑定**(M205):`download/config.rs` 那个常量是宿主读数用的
|
||||
/// 唯一键名,本文件 `specs()` 里是 schema 的登记——两侧各写一个字符串,此前没有任何断言把它们
|
||||
/// 绑在一起,常量被改名/写错时全绿,行为只在宿主侧静默退化成 `Builtin`(用户填的 `off` 被忽略)。
|
||||
///
|
||||
/// 抓得住:常量值写错(如 `download.ghproxy`)⇒ 找不到登记项,第一句红;登记的 `max_len` 从 256
|
||||
/// 改掉、或默认值从空串改成别的 ⇒ 各自红(类型/默认值是宿主的读侧契约)。
|
||||
/// 抓不住:两侧**同时**改名(那是一次公开改名,本用例判断不了它对不对,只能保证两侧一致)。
|
||||
#[test]
|
||||
fn the_gh_proxy_key_is_bound_to_the_schema_registration() {
|
||||
use crate::download::config::KEY_GH_PROXY;
|
||||
use crate::setting::spec::ValueKind;
|
||||
|
||||
let Some(gh) = specs().into_iter().find(|spec| spec.key == KEY_GH_PROXY) else {
|
||||
panic!("下载键常量 {KEY_GH_PROXY:?} 在 schema 里没有登记项(常量值与 specs() 的键名不一致)");
|
||||
};
|
||||
assert_eq!(gh.kind, ValueKind::Text { max_len: 256 }, "{}", gh.key);
|
||||
assert_eq!(gh.default, serde_json::json!(""), "{}", gh.key);
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,26 @@ use unlucky_core::download::{DownloadRequest, ResourceKind};
|
||||
use super::super::env::HostEnv;
|
||||
|
||||
/// 站点要求的 User-Agent(Modrinth 明文要求带联系方式,否则限流)。
|
||||
pub(super) const USER_AGENT: &str =
|
||||
"Unlucky-Launcher/0.1 (+https://github.com/AskaEth/Unlucky-Launcher)";
|
||||
///
|
||||
/// ## 为什么这条 UA 要单独看
|
||||
///
|
||||
/// 它**只**出现在带 header 的那条路径(`engine.client()` 直发,今天是 CurseForge 的 `X-API-KEY`),
|
||||
/// 而 Modrinth 的请求走 [`get_via_engine`] —— 那条用的是引擎自己的 UA(`EngineConfig::user_agent`)。
|
||||
/// 也就是说进程里同时存在两个 UA 字符串,改一处不会带动另一处。
|
||||
///
|
||||
/// ## 版本号来自 `CARGO_PKG_VERSION`(M205)
|
||||
///
|
||||
/// 此前写死 `Unlucky-Launcher/0.1`,而当时的版本已经是 0.2.3:写死的版本号只会一年一年地落后,而
|
||||
/// Modrinth 按 UA 做限流与「出问题怎么联系你」的登记 —— UA 里说错版本,就是给了错信息。
|
||||
///
|
||||
/// **产品名与联系方式的写法不跟着版本动**:`Unlucky-Launcher/<version> (+<repo URL>)` 这个形状、
|
||||
/// 以及那个 GitHub 地址,是站点侧识别我们、以及知道去哪儿联系的地方;改它是单独的产品决定,不在
|
||||
/// 「版本号来源」这条改动里顺手改(断言 `the_user_agent_carries_the_package_version` 钉住了现状)。
|
||||
pub(super) const USER_AGENT: &str = concat!(
|
||||
"Unlucky-Launcher/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (+https://github.com/AskaEth/Unlucky-Launcher)"
|
||||
);
|
||||
|
||||
/// 最多重试次数(含首次)。
|
||||
///
|
||||
@@ -149,6 +167,29 @@ mod tests {
|
||||
use super::get;
|
||||
use crate::host::env::HostEnv;
|
||||
|
||||
/// UA 里的版本号必须等于这个 crate 的 `CARGO_PKG_VERSION`(M205)。
|
||||
///
|
||||
/// 为什么不是「编译过就行」:`concat!` 只保证拼得起来,**写死**一个版本号照样编得过 —— 上一版正是
|
||||
/// 这样从 0.1 一路落后到 0.2.3(UA 会发到站点,版本号是站点侧看到的事实)。
|
||||
///
|
||||
/// 抓得住:版本号又写回任何字面量(`0.1` / `0.2` / 与 `CARGO_PKG_VERSION` 不等的任何值)⇒ 红;
|
||||
/// UA 形状被拼歪(少了 `Unlucky-Launcher/` 前缀、`(+<联系方式>)` 那一段被删)⇒ 也红。
|
||||
/// 抓不住:真实 Modrinth 请求下这条 UA 会被怎么限流(单测打不了公网);也抓不住「产品名与联系
|
||||
/// 方式写得对不对」——这里只是钉住现状不许顺手改,不是对站点口径的验证。
|
||||
#[test]
|
||||
fn the_user_agent_carries_the_package_version() {
|
||||
let ua = super::USER_AGENT;
|
||||
let version = ua
|
||||
.strip_prefix("Unlucky-Launcher/")
|
||||
.and_then(|rest| rest.split(' ').next())
|
||||
.expect("UA 形状应为 `Unlucky-Launcher/<version> (<联系方式>)`");
|
||||
assert_eq!(version, env!("CARGO_PKG_VERSION"), "UA: {ua}");
|
||||
assert!(
|
||||
ua.contains("(+https://github.com/AskaEth/Unlucky-Launcher)"),
|
||||
"联系方式那一段是站点要求,不许顺手删:{ua}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 离线回环 HTTP 服务:只答固定 JSON,数「处理了几条请求 / 接了几条连接」。
|
||||
///
|
||||
/// 用 127.0.0.1 而不是真站点:**单测不许打公网**(CI 没网,也不该有)。
|
||||
|
||||
Reference in new issue
Block a user