feat(m193): 下载并发/重试/gh-proxy 真接进引擎(schema 与引擎同源常量)

- download.concurrency / download.retry_limit 以前引擎侧零消费者(恒用
  EngineConfig::default() 的 64,而 schema 说 8)。现在默认值与硬上限收成
  download/config.rs 的常量,schema、EngineConfig::default()、Semaphore 容量
  三处同源;宿主把三个键翻译成 DownloadTuning 后经 shared::set_tuning(按数据根)
  灌进装配点,改了设置下一次取引擎就生效。
- download.gh_proxy 三态(留空=跟随内置默认 / off=关 / 前缀=覆盖)下沉到
  shared::build,所有走共享引擎的下载路径(内容库/版本清单/字体/安装)一致。
- 删掉 MirrorTable::set_gh_proxy 与 default_gh_proxy 两个改了没用的入口。
- 下载设置页把三条假演示值换成说语义的中文行(含三态说明与 1~64 硬上限)。
- 守卫:探针读 Semaphore 真实许可数(不是读配置),每条配变异证明。
- docs/download-settings.md 记下取证表(另四个键仍零消费,已立 finding)。
This commit is contained in:
worker-dlmsettings authored and AskaEth committed 2026-11-09 05:24:11 +08:00
1 parent 98262d3835
commit e8544b19a7
15 files changed
+958 -200

No files matched your search

+5
View File
@@ -226,6 +226,11 @@ impl CoreHost {
if key == super::engine::DETECT_REGION_ACTION {
self.refresh_update_factory();
}
// M193:下载三键(并发/重试/gh-proxy)改了要立刻换引擎参数。这是**显式**的同步点
// ——不藏在任何读函数里。值没变时它一行不动。
if settings::download::apply_tuning(&self.env) {
self.refresh_update_factory();
}
self.send(CoreEvent::SettingsChanged(settings::groups(&self.env)));
self.java_chain.clear();
self.refresh_java();
+11 -1
View File
@@ -22,8 +22,14 @@ use super::env::HostEnv;
/// 取该数据根上**共享**的下载引擎(缓存/健康度/地区判定都在数据根里)。
///
/// 同一个数据根无论被请求多少次,都是同一个引擎;不同数据根各一份(缓存/健康度不串味)。
///
/// **取引擎前先把下载设置灌进 core 的下载调优**(M193,按数据根存):`download.concurrency` /
/// `download.retry_limit` / `download.gh_proxy` 变了就重建引擎(值没变时 [`settings::download::apply_tuning`]
/// 一行不动,所以每次调用都灌是安全的)。这是「用户改了设置 ⇒ 下一次下载真的按新值跑」的落点:
/// 不在这里灌,进程级引擎会带着启动时那份参数活到进程结束——那正是「设置页看着有、实际没用」。
#[must_use]
pub fn engine(env: &HostEnv) -> Arc<DownloadEngine> {
super::settings::download::apply_tuning(env);
shared::engine_for(env.layout.cache())
}
@@ -120,7 +126,11 @@ mod tests {
let dir = root("cache");
let env = HostEnv::at_root(&dir);
let engine = engine(&env);
assert_eq!(engine.config().max_concurrency, 64);
assert_eq!(
engine.config().max_concurrency,
unlucky_core::download::config::DEFAULT_MAX_CONCURRENCY
);
assert_eq!(engine.concurrency_probe(), engine.config().max_concurrency);
// 地区判定不联网:只读缓存/信号
let _ = domestic(&env);
std::fs::remove_dir_all(&dir).ok();
+5 -84
View File
@@ -13,10 +13,14 @@ use crate::dto::{SettingGroupView, SettingRowView};
use super::env::HostEnv;
// M193:下载组的行与「设置 → 引擎调优」的翻译搬进子模块(`settings.rs` 顶在 400 行硬线上,
// 理由与 `settings_tests.rs` 同源,见那个文件的文件头)。
pub(crate) mod download;
/// 全部设置分组(设置页右栏一次拿全,切分组不再请求)。
pub fn groups(env: &HostEnv) -> Vec<SettingGroupView> {
vec![
download_group(env),
download::group(env),
java_group(env),
data_group(env),
ai_group(env),
@@ -58,89 +62,6 @@ fn selected(options: &[&str], value: &str) -> i32 {
.unwrap_or(0)
}
/// 下载(D7):并发/重试/地区/镜像偏好/代理。
fn download_group(env: &HostEnv) -> SettingGroupView {
let concurrency = int(env, "download.concurrency", 8).to_string();
let retry = int(env, "download.retry_limit", 3).to_string();
let concurrency_options = ["4", "8", "16"];
let retry_options = ["0", "1", "3", "5"];
let regions = ["auto", "cn", "global"];
let mirrors = ["auto", "official", "mirror"];
let proxies = ["none", "system", "manual"];
let region_value = text(env, "download.region");
let mirror_value = text(env, "download.mirror_preference");
let proxy_value = text(env, "download.proxy_mode");
SettingGroupView::new("download", "下载与镜像(D7)")
.with(SettingRowView::choice(
"download.concurrency",
"并发数",
"同时下载的文件数(海量小文件,16 路足够压满带宽)",
&concurrency_options,
selected(&concurrency_options, &concurrency),
))
.with(SettingRowView::choice(
"download.retry_limit",
"单文件重试次数",
"失败后换源重试的上限",
&retry_options,
selected(&retry_options, &retry),
))
.with(SettingRowView::choice(
"download.region",
"地区判定",
"auto = 时区+语言+探测;决定是否优先国内镜像",
&regions,
selected(
&regions,
if region_value.is_empty() {
"auto"
} else {
&region_value
},
),
))
.with(SettingRowView::choice(
"download.mirror_preference",
"镜像偏好",
"auto = 按健康度评分;official = 官方源优先",
&mirrors,
selected(
&mirrors,
if mirror_value.is_empty() {
"auto"
} else {
&mirror_value
},
),
))
.with(SettingRowView::choice(
"download.proxy_mode",
"代理模式",
"none / system(跟随系统)/ manual(下面填地址)",
&proxies,
selected(
&proxies,
if proxy_value.is_empty() {
"none"
} else {
&proxy_value
},
),
))
.with(SettingRowView::text(
"download.proxy_url",
"HTTP 代理",
"manual 模式生效;留空则用系统代理",
&text(env, "download.proxy_url"),
))
.with(SettingRowView::action(
super::engine::DETECT_REGION_ACTION,
"重新检测地区",
"清掉 24h 地区缓存,下次下载重新判定",
"detect_region",
))
}
/// Java(D13/D23):自动安装/内存/选路/发行版优先级。
fn java_group(env: &HostEnv) -> SettingGroupView {
let memory = int(env, "java.default_memory_mb", 2048).to_string();
@@ -0,0 +1,174 @@
//! 设置页「下载」分组 + 下载调优的**宿主侧翻译**(M193)。
//!
//! ## 为什么单开一个文件
//!
//! 同 `host/settings_tests.rs` 的理由:`host/settings.rs` 正好顶在 400 行硬线上,而本轮要往这一组加
//! 「`download.gh_proxy` 三态行 + 并发硬上限说明 + 调优翻译」约 24 行。不拆就得砍 `settings.rs` 里
//! 那些**留着防复发**的论证注释(`ai_group` 的 M174 回归说明、`parse_value` 的 P1-1 浮点键事故)
//! ——那比多一个文件糟得多。搬出来还有一个附带好处:这一组是**唯一**要往 core 灌下载调优的一组,
//! 代码与它的守卫(`settings_tests.rs`)现在挨在一起。
//!
//! ## 谁在读 / 谁在写
//!
//! ```text
//! config/launcher.json ←── store.set(`settings::set`,本模块只读)
//! download.concurrency ─┐
//! download.retry_limit ─┼─→ DownloadTuning(core 决定默认值与夹取)─→ shared::set_tuning
//! download.gh_proxy ───┘ (引擎重建时生效)
//! ```
//!
//! **调优的同步点是显式的**:`host/engine.rs` 每次取引擎前、`dispatch.rs` 的 `SetSetting` 分支里,
//! 都调 [`apply_tuning`](幂等:值没变就什么都不做)。它**不是**藏在某个读函数里的副作用——
//! 「读起来是纯读、实际改全局状态」正是本仓吃过亏的那类形状。
use unlucky_core::download::config::{
DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY,
};
use unlucky_core::download::shared;
use crate::dto::{SettingGroupView, SettingRowView};
use super::super::env::HostEnv;
use super::{int, selected, text};
/// 并发数的推荐档位(`selected()` 找不到时回 0;手改出来的非档位值会被插进选项表,见 [`group`])。
const CONCURRENCY_LADDER: [&str; 5] = ["4", "8", "16", "32", "64"];
/// 单源尝试次数的推荐档位。
const RETRY_LADDER: [&str; 5] = ["1", "2", "3", "5", "10"];
/// 下载(D7):并发/重试/地区/镜像偏好/代理/GitHub 加速前缀。
pub(super) fn group(env: &HostEnv) -> SettingGroupView {
let concurrency = int(
env,
unlucky_core::download::config::KEY_CONCURRENCY,
DEFAULT_MAX_CONCURRENCY as i64,
)
.to_string();
let retry = int(
env,
unlucky_core::download::config::KEY_RETRY_LIMIT,
i64::from(DEFAULT_MAX_ATTEMPTS_PER_SOURCE),
)
.to_string();
let concurrency_options = ladder(&CONCURRENCY_LADDER, &concurrency);
let retry_options = ladder(&RETRY_LADDER, &retry);
let concurrency_options: Vec<&str> = concurrency_options.iter().map(String::as_str).collect();
let retry_options: Vec<&str> = retry_options.iter().map(String::as_str).collect();
let regions = ["auto", "cn", "global"];
let mirrors = ["auto", "official", "mirror"];
let proxies = ["none", "system", "manual"];
let region_value = text(env, "download.region");
let mirror_value = text(env, "download.mirror_preference");
let proxy_value = text(env, "download.proxy_mode");
SettingGroupView::new("download", "下载与镜像(D7)")
.with(SettingRowView::choice(
unlucky_core::download::config::KEY_CONCURRENCY,
"并发数",
"同时下载的文件数:出厂 8,可调 1~64(引擎硬上限 64,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效",
&concurrency_options,
selected(&concurrency_options, &concurrency),
))
.with(SettingRowView::choice(
unlucky_core::download::config::KEY_RETRY_LIMIT,
"单源尝试次数",
"同一个源最多试几次(含首次;出厂 3)。换源不受此限:候选链上有几个源就各给几次",
&retry_options,
selected(&retry_options, &retry),
))
.with(SettingRowView::choice(
"download.region",
"地区判定",
"auto = 时区+语言+探测;决定是否优先国内镜像",
&regions,
selected(
&regions,
if region_value.is_empty() {
"auto"
} else {
&region_value
},
),
))
.with(SettingRowView::choice(
"download.mirror_preference",
"镜像偏好",
"auto = 按健康度评分;official = 官方源优先",
&mirrors,
selected(
&mirrors,
if mirror_value.is_empty() {
"auto"
} else {
&mirror_value
},
),
))
.with(SettingRowView::choice(
"download.proxy_mode",
"代理模式",
"none / system(跟随系统)/ manual(下面填地址)",
&proxies,
selected(
&proxies,
if proxy_value.is_empty() {
"none"
} else {
&proxy_value
},
),
))
.with(SettingRowView::text(
"download.proxy_url",
"HTTP 代理",
"manual 模式生效;留空则用系统代理",
&text(env, "download.proxy_url"),
))
// M193:gh-proxy 三态。**留空与 off 是两件事**,描述里必须都写出来——用户最容易把
// 「我还没决定」当成「我不要加速」,于是以为自己关掉了、实际还在套前缀。
.with(SettingRowView::text(
unlucky_core::download::config::KEY_GH_PROXY,
"GitHub 加速前缀(gh-proxy)",
"留空 = 跟随内置默认(镜像优先/国内判定时自动套 gh-proxy.com);填 off = 关闭(不生成任何 gh-proxy 候选);填一个 URL 前缀 = 覆盖它",
&text(env, unlucky_core::download::config::KEY_GH_PROXY),
))
.with(SettingRowView::action(
super::super::engine::DETECT_REGION_ACTION,
"重新检测地区",
"清掉 24h 地区缓存,下次下载重新判定",
"detect_region",
))
}
/// 推荐档位 + 「当前值不在档位里就把它插进去」(否则这一行会把 2 显示成 4 —— 又一个假值)。
fn ladder(ladder: &[&str], current: &str) -> Vec<String> {
let mut options: Vec<String> = ladder.iter().map(|item| (*item).to_string()).collect();
if !current.is_empty() && !options.iter().any(|option| option == current) {
options.push(current.to_string());
options.sort_by_key(|option| option.parse::<i64>().unwrap_or(i64::MAX));
}
options
}
/// 从 `launcher.json` 读出下载调优(默认值与夹取口径在 core,本函数只做「取原始值」)。
#[must_use]
pub(crate) fn tuning_of(env: &HostEnv) -> DownloadTuning {
use unlucky_core::download::config::{KEY_CONCURRENCY, KEY_GH_PROXY, KEY_RETRY_LIMIT};
let store = env.store();
let doc = store.doc();
let gh = doc
.effective_str(KEY_GH_PROXY)
.map(str::to_string)
.unwrap_or_default();
DownloadTuning::from_settings(
doc.effective_i64(KEY_CONCURRENCY),
doc.effective_i64(KEY_RETRY_LIMIT),
Some(gh.as_str()),
)
}
/// 把当前设置灌进 core 的下载调优(按数据根),返回**是否真的变了**(变了就失效该根的引擎)。
///
/// 幂等:值没变时 `shared::set_tuning` 一行不动,所以「每次取引擎前都调」是安全的。
pub(crate) fn apply_tuning(env: &HostEnv) -> bool {
shared::set_tuning(env.layout.cache(), tuning_of(env))
}
@@ -1,4 +1,8 @@
//! host/settings.rs 的单测(拆文件守 400 行上限)。
//!
//! **为什么单独一个文件**:`settings.rs` 正好顶在 400 行硬线上,而「设置写进去到底有没有生效」
//! 这一类用例恰恰需要反复构造数据根 + 读写 `launcher.json`,塞回生产文件就只剩砍论证注释这一条路。
//! 本文件与它守的代码同目录、同名族(`settings.rs` 的 `#[path]` 挂载),**不是**随手放的杂项目。
use super::*;
@@ -7,6 +11,51 @@ fn env_in(dir: &std::path::Path) -> HostEnv {
HostEnv::at_root(dir)
}
/// **M193 守门**:把设置改成 2 ⇒ **下一次取到的引擎**的并发闸门真是 2(探针读 `Semaphore`)。
///
/// 这是「设置项说假话」那条缺陷族的正面守门。断言分三段,缺一段都守不住:
///
/// 1. 写设置(走 `SetSetting` 同一条 `settings::set`);
/// 2. 走宿主真正的取引擎入口 `host::engine::engine(env)`(它负责把设置灌进 core 调优);
/// 3. 读**引擎的真实闸门** `concurrency_probe()`,不是 `config().max_concurrency`。
///
/// 变异证明(只改实现侧):
/// * 把 `host/engine.rs::engine` 里那行 `apply_tuning` 删掉 ⇒ 引擎按出厂 8 建,探针读到 8,红;
/// * 把 `core::download::shared::build` 里的 `tune.apply_to(EngineConfig::default())` 换回
/// `EngineConfig::default()` ⇒ 同样读到 8,红;
/// * 把 `settings/download.rs::tuning_of` 读错键名(例如 `download.threads`)⇒ 读到默认 8,红。
///
/// `download.gh_proxy` 那一路**没有**端到端用例:那个键由 M197 登记,尚未合入 dev,而
/// `setting::document` 只认已登记的键(未登记的键进不了 `effective()`),所以这里写进文件也读不出来。
/// core 侧有「调优三档 → 候选链」的守卫(`download/shared.rs`),端到端那一半登记为 M197 的 follow-up。
#[test]
fn the_concurrency_setting_reaches_the_real_download_engine() {
let dir = std::env::temp_dir().join(format!("ull-host-tune-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let env = env_in(&dir);
set(&env, "download.concurrency", "2").expect("写并发");
set(&env, "download.retry_limit", "1").expect("写尝试次数");
let engine = crate::host::engine::engine(&env);
assert_eq!(engine.concurrency_probe(), 2, "闸门必须真是 2 张许可");
assert_eq!(
engine.config().max_attempts_per_source,
1,
"尝试次数也要吃到设置"
);
// 改回去 ⇒ 下一次取到的引擎跟着换(不是「重启才生效」)。
set(&env, "download.concurrency", "16").expect("再写并发");
let rebuilt = crate::host::engine::engine(&env);
assert_eq!(rebuilt.concurrency_probe(), 16);
assert!(
!std::sync::Arc::ptr_eq(&engine, &rebuilt),
"参数变了必须换引擎,否则新值落不到在飞的那份上"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn groups_cover_the_six_required_areas() {
let dir = std::env::temp_dir().join(format!("ull-host-settings-{}", std::process::id()));