feat(m203): CLI 三条下载命令改吃 download.* 设置(并发/重试/gh-proxy)

`ull install` / `ull java` / `ull pack` 此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
用户在设置里配的 `download.concurrency` / `retry_limit` / `gh_proxy` 一概不生效;M193 把出厂并发
从 64 改成 8 之后,它们还静默跟着降了。现在三处都经 `crates/cli/src/download.rs::engine(ctx)`:

- core:`shared/tuned.rs` 新增 `engine_with_tuning(cache_root, tune)` = `set_tuning` + `engine_for`,
  由 `shared.rs` 重导出;`engine_for` 的签名与行为一字未改(presenter 的两步语义原样保留)。
  放子模块是为了不动 `engine.rs`(那是 400 行零余量),`shared.rs` 仍 398 行。
- CLI:`download::tuning(ctx)` 用 `SettingStore::load(ctx.launcher_config(), launcher_schema())` 读
  三个键(与 `ull config set` 同一份文件),默认值/夹取/回落在 `DownloadTuning::from_settings`;
  键缺失或坏值静默回落出厂值,`engine()` 没有错误通道,安装不会因此中断。
- 顺带:`ull update` 复用 `install::build_engine`,也随之吃到设置。
- 测试:`crates/cli/src/download/tests.rs` 三条命令各一条(读 `config().max_concurrency` 与
  `concurrency_probe()` 的真实值)+ 一条「缺失/越界/负数/类型不对 ⇒ 静默回落出厂值」;
  `shared/tuned.rs` 两条守住「灌调优与取引擎是一步」。变异实测:pack 换回裸 `EngineConfig::default()`
  ⇒ 只有 pack 那条红(left 8 / right 2)。
This commit is contained in:
AskaEth committed 2026-11-09 10:07:41 +08:00
1 parent c97630d59e
commit b31ca636d1
9 files changed
+368 -81

No files matched your search

+5 -27
View File
@@ -19,12 +19,7 @@ mod vanilla;
use std::sync::Arc; use std::sync::Arc;
use unlucky_core::addon::loader::LoaderKind; use unlucky_core::addon::loader::LoaderKind;
use unlucky_core::download::cache::now_ms; use unlucky_core::download::{DownloadEngine, DownloadErrorKind};
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
use unlucky_core::download::region_system::detect_signals;
use unlucky_core::download::{
ContentCache, DownloadEngine, DownloadErrorKind, EngineConfig, HealthRegistry, MirrorTable,
};
use unlucky_core::game::install::{InstallError, InstallReport, Registration}; use unlucky_core::game::install::{InstallError, InstallReport, Registration};
use crate::cli::InstallArgs; use crate::cli::InstallArgs;
@@ -78,27 +73,10 @@ pub(super) fn register_instance(
.map_err(|error| CliError::user("instance_register", format!("登记实例失败:{error}"))) .map_err(|error| CliError::user("instance_register", format!("登记实例失败:{error}")))
} }
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。 /// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,装配参数来自设置(并发 / 重试 / gh-proxy,
/// /// 读数口径见 [`crate::download`])——不再用裸 `EngineConfig::default()`(M203)。
/// 地区判定**不做网络探测**(P2 首版;探测开关属设置层接线), pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
/// 因此中文环境默认镜像优先、其它环境直连——与 D7 的"不探测时"约定一致。 crate::download::engine(ctx)
pub(super) fn build_engine(ctx: &Ctx) -> DownloadEngine {
let layout = ctx.layout();
let cache_root = layout.cache();
let now = now_ms();
let region_path = cache_root.join("region.json");
let region = load_cached(&region_path, now).unwrap_or_else(|| {
let profile = decide_from(&detect_signals(), now);
let _ = store_cached(&region_path, &profile);
profile
});
DownloadEngine::new(
EngineConfig::default(),
Arc::new(MirrorTable::builtin()),
Arc::new(region),
Arc::new(HealthRegistry::open_in(&cache_root, now)),
Arc::new(ContentCache::open(&cache_root)),
)
} }
/// 同名但绑定别的版本 → 冲突;同名同版本视为重装(复用实例,允许)。 /// 同名但绑定别的版本 → 冲突;同名同版本视为重装(复用实例,允许)。
+5 -24
View File
@@ -17,12 +17,7 @@ use std::sync::Arc;
use serde_json::{json, Value}; use serde_json::{json, Value};
use unlucky_core::download::blocking::block_on; use unlucky_core::download::blocking::block_on;
use unlucky_core::download::cache::now_ms; use unlucky_core::download::{DownloadEngine, RegionProfile};
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
use unlucky_core::download::region_system::detect_signals;
use unlucky_core::download::{
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable, RegionProfile,
};
use unlucky_core::game::java::{ use unlucky_core::game::java::{
self, location, vendor, InstallRequest, JavaOrigin, JavaRuntime, JdkVendor, LocationMode, self, location, vendor, InstallRequest, JavaOrigin, JavaRuntime, JdkVendor, LocationMode,
DEFAULT_MAJOR, DEFAULT_MAJOR,
@@ -312,24 +307,10 @@ fn install_root_of(runtime: &JavaRuntime) -> Option<String> {
.map(|dir| dir.display().to_string()) .map(|dir| dir.display().to_string())
} }
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。 /// 建下载引擎:装配参数来自设置(并发 / 重试 / gh-proxy,读数口径见 [`crate::download`]),
fn build_engine(ctx: &Ctx) -> DownloadEngine { /// 引擎本体与 `install` / `pack` 共用同一个(core 按缓存根共享)。
let layout = ctx.layout(); pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
let cache_root = layout.cache(); crate::download::engine(ctx)
let now = now_ms();
let region_path = cache_root.join("region.json");
let region = load_cached(&region_path, now).unwrap_or_else(|| {
let profile = decide_from(&detect_signals(), now);
let _ = store_cached(&region_path, &profile);
profile
});
DownloadEngine::new(
EngineConfig::default(),
Arc::new(MirrorTable::builtin()),
Arc::new(region),
Arc::new(HealthRegistry::open_in(&cache_root, now)),
Arc::new(ContentCache::open(&cache_root)),
)
} }
/// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。 /// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。
+5 -24
View File
@@ -30,12 +30,7 @@ use unlucky_core::addon::import::{
ImportReport, ImportRequest, ImportStatus, ImportReport, ImportRequest, ImportStatus,
}; };
use unlucky_core::download::blocking::block_on; use unlucky_core::download::blocking::block_on;
use unlucky_core::download::cache::now_ms; use unlucky_core::download::DownloadEngine;
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
use unlucky_core::download::region_system::detect_signals;
use unlucky_core::download::{
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable,
};
use crate::cli::PackImportArgs; use crate::cli::PackImportArgs;
use crate::context::Ctx; use crate::context::Ctx;
@@ -286,24 +281,10 @@ fn count_action(report: &ImportReport, action: FileAction) -> usize {
.count() .count()
} }
/// 与 `install` 共用同一套引擎构造(镜像 / 地区 / 健康度 / 内容缓存)。 /// 与 `install` / `java` 共用同一套引擎装配(镜像 / 地区 / 健康度 / 内容缓存),
fn build_engine(ctx: &Ctx) -> DownloadEngine { /// 参数来自设置:读数口径见 [`crate::download`]。
let layout = ctx.layout(); pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
let cache_root = layout.cache(); crate::download::engine(ctx)
let now = now_ms();
let region_path = cache_root.join("region.json");
let region = load_cached(&region_path, now).unwrap_or_else(|| {
let profile = decide_from(&detect_signals(), now);
let _ = store_cached(&region_path, &profile);
profile
});
DownloadEngine::new(
EngineConfig::default(),
Arc::new(MirrorTable::builtin()),
Arc::new(region),
Arc::new(HealthRegistry::open_in(&cache_root, now)),
Arc::new(ContentCache::open(&cache_root)),
)
} }
#[cfg(test)] #[cfg(test)]
+1 -2
View File
@@ -34,7 +34,6 @@
//! 去掉 `--dest` 会把它真改名换掉(`crates/cli/tests/update_e2e_test.rs` 的注释写了这件事)。 //! 去掉 `--dest` 会把它真改名换掉(`crates/cli/tests/update_e2e_test.rs` 的注释写了这件事)。
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use serde_json::json; use serde_json::json;
use unlucky_core::update::{default_source, Applied, CheckOutcome, Job, UpdateError, Version}; use unlucky_core::update::{default_source, Applied, CheckOutcome, Job, UpdateError, Version};
@@ -57,7 +56,7 @@ fn current_version() -> Result<Version, CliError> {
/// 造一个内核 job(源 = GitHub / `ULL_UPDATE_DIR`,暂存 = 数据根缓存,目标 = 本进程 exe)。 /// 造一个内核 job(源 = GitHub / `ULL_UPDATE_DIR`,暂存 = 数据根缓存,目标 = 本进程 exe)。
fn job(ctx: &Ctx, dest: Option<PathBuf>) -> Job { fn job(ctx: &Ctx, dest: Option<PathBuf>) -> Job {
let engine = super::install::build_engine(ctx); let engine = super::install::build_engine(ctx);
let job = Job::for_process(default_source(Arc::new(engine)), &ctx.layout().cache()); let job = Job::for_process(default_source(engine), &ctx.layout().cache());
match dest { match dest {
Some(dest) => job.with_dest(dest), Some(dest) => job.with_dest(dest),
None => job, None => job,
+56
View File
@@ -0,0 +1,56 @@
//! CLI 侧的下载引擎:把 CLI 自己的 `launcher.json` 接到 core 的下载调优上(M203)。
//!
//! 三条下载命令(`install` / `java` / `pack`)此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
//! 用户在设置里配的 `download.concurrency` / `download.retry_limit` / `download.gh_proxy` 一概不生效。
//! M193 把出厂并发从 64 改成 8 之后,它们还**静默**跟着降了——「设置项说假话」的同期病灶
//! (`docs/download-settings.md` §3.1 记着这三条 ❌)。
//!
//! 现在三处一律经 [`engine`] 取引擎(进程内共享,见 `core::download::shared`):
//!
//! ```text
//! <数据根>/config/launcher.json(与 `ull config set` 同一份) → setting store → DownloadTuning
//! (默认值/夹取/回落在 core)
//! → shared::engine_with_tuning(<cache 根>, tuning) → 引擎的 Semaphore / attempts_for()
//! ```
//!
//! **回落是静默的**:`SettingStore::load` 永不失败(坏值只进 `problems()`);类型不对 → `None`,
//! 越界 → `DownloadTuning::from_settings` 的区间过滤。两种情况都落成出厂值,**不报错、不中断安装**。
use std::sync::Arc;
use unlucky_core::download::config::{
DownloadTuning, KEY_CONCURRENCY, KEY_GH_PROXY, KEY_RETRY_LIMIT,
};
use unlucky_core::download::{shared, DownloadEngine};
use unlucky_core::setting::keys::launcher_schema;
use unlucky_core::setting::store::SettingStore;
use crate::context::Ctx;
/// 从 CLI 自己的 setting store 读下载调优(`launcher.json` 的三个 `download.*` 键)。
///
/// 读的是 `ctx.launcher_config()`——与 `ull config set download.concurrency` 的落点同一份文件
/// (设置键的落点分流见 [`Ctx::setting_path_for`])。gh-proxy 空串 = 跟随内置默认,**不是**关。
#[must_use]
pub(crate) fn tuning(ctx: &Ctx) -> DownloadTuning {
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
let doc = store.doc();
let gh_proxy = doc
.effective_str(KEY_GH_PROXY)
.unwrap_or_default()
.to_string();
DownloadTuning::from_settings(
doc.effective_i64(KEY_CONCURRENCY),
doc.effective_i64(KEY_RETRY_LIMIT),
Some(gh_proxy.as_str()),
)
}
/// 建该数据根的下载引擎:参数来自 [`tuning`],引擎本体由 core 按缓存根共享。
#[must_use]
pub(crate) fn engine(ctx: &Ctx) -> Arc<DownloadEngine> {
shared::engine_with_tuning(ctx.layout().cache(), tuning(ctx))
}
#[cfg(test)]
mod tests;
+203
View File
@@ -0,0 +1,203 @@
//! [`super`] 的单测(M203):`launcher.json` 的 `download.*` 真的进到三条命令拿到的引擎里。
//!
//! 断言读的是**引擎的真实字段**——`config().max_concurrency` 与 `Semaphore` 的
//! `available_permits()`(`concurrency_probe`)——不是「函数被调用过」:后者在回落写错、
//! 或者命令又自建了一个裸引擎时照样绿。
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::{json, Value};
use unlucky_core::download::config::{
DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY,
};
use unlucky_core::download::DownloadEngine;
use unlucky_core::setting::keys::launcher_schema;
use unlucky_core::setting::store::SettingStore;
use crate::cli::Globals;
use crate::context::Ctx;
/// 一次性临时目录(cli 没有 dev-dependency 的 tempfile;与 `commands/config/tests.rs` 同一手法)。
struct TempDir(PathBuf);
impl TempDir {
fn new(tag: &str) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_nanos());
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"ull-cli-download-{tag}-{}-{seq}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&path).expect("建临时目录");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// 数据根钉在临时目录里:`launcher.json` 落在 `<dir>/.ULL/config/launcher.json`。
fn ctx_at(dir: &TempDir) -> Ctx {
let globals = Globals {
data_dir: Some(dir.path().to_path_buf()),
..Globals::default()
};
Ctx::resolve(&globals).expect("解析数据根")
}
/// 直接写 `launcher.json`——**与 `ull config set` 的落点是同一份文件**(`ctx.launcher_config()`),
/// 所以这条用例测的就是用户手改/命令改之后 CLI 读到的值。
fn write_launcher_config(ctx: &Ctx, document: Value) {
let path = ctx.launcher_config();
std::fs::create_dir_all(path.parent().expect("配置目录")).expect("建配置目录");
std::fs::write(&path, serde_json::to_vec(&document).expect("序列化"))
.expect("写 launcher.json");
}
/// 「引擎真的按这个并发跑」:配置字段 + 闸门许可数,两处都读真实值。
fn assert_concurrency(engine: &DownloadEngine, expected: usize, case: &str) {
assert_eq!(
engine.config().max_concurrency,
expected,
"{case}:`EngineConfig` 里的并发"
);
assert_eq!(
engine.concurrency_probe(),
expected,
"{case}:`Semaphore` 真实许可数"
);
}
/// **`install` 吃设置**:`download.concurrency` 改了就变;同一次进程里再改一次也立刻变(不是只读一次)。
///
/// 抓得住的变异:`commands::install::build_engine` 换回裸 `EngineConfig::default()` ⇒ 3 / 5 两处红。
/// 抓不住的:真实下载时「只有 3 条连接在飞」的时序(离线验不了,见报告「没验的部分」)。
#[test]
fn install_engine_takes_the_concurrency_from_the_setting_store() {
let dir = TempDir::new("install");
let ctx = ctx_at(&dir);
// 没有 launcher.json = 没配过 ⇒ 出厂值(这也是「回落」的第一档)。
assert_concurrency(
&crate::commands::install::build_engine(&ctx),
DEFAULT_MAX_CONCURRENCY,
"未配置",
);
write_launcher_config(&ctx, json!({ "download": { "concurrency": 3 } }));
assert_concurrency(
&crate::commands::install::build_engine(&ctx),
3,
"concurrency=3",
);
write_launcher_config(&ctx, json!({ "download": { "concurrency": 5 } }));
assert_concurrency(
&crate::commands::install::build_engine(&ctx),
5,
"concurrency=5",
);
}
/// **`java` 吃设置**:并发与每个源的尝试次数都进引擎。
///
/// 抓得住的变异:`commands::java::build_engine` 换回裸 `EngineConfig::default()` ⇒ 探针停在 8、次数停在 3,红。
#[test]
fn java_engine_takes_the_concurrency_and_retry_from_the_setting_store() {
let dir = TempDir::new("java");
let ctx = ctx_at(&dir);
write_launcher_config(
&ctx,
json!({ "download": { "concurrency": 3, "retry_limit": 4 } }),
);
let engine = crate::commands::java::build_engine(&ctx);
assert_concurrency(&engine, 3, "concurrency=3");
assert_eq!(
engine.config().max_attempts_per_source,
4,
"retry_limit=4 要真的落到引擎上(出厂是 3)"
);
}
/// **`pack` 吃设置**(三条命令各有一条独立断言:把任意一处改回裸引擎,只有那一条红)。
///
/// 抓得住的变异:`commands::pack::build_engine` 换回裸 `EngineConfig::default()` ⇒ 2 处红。
#[test]
fn pack_engine_takes_the_concurrency_from_the_setting_store() {
let dir = TempDir::new("pack");
let ctx = ctx_at(&dir);
write_launcher_config(&ctx, json!({ "download": { "concurrency": 2 } }));
assert_concurrency(
&crate::commands::pack::build_engine(&ctx),
2,
"concurrency=2",
);
}
/// **反向断言**:键缺失 / 越界 / 负数 / 类型不对 ⇒ 回落出厂值,而且回落是**静默的**。
///
/// 「静默」在这里是结构性的:`crate::download::engine` 的返回类型没有错误通道,坏设置只进
/// `SettingStore::problems()`,调用方(安装流程)连一个 `?` 都不用加——`ull install` 不会因为一条
/// 写坏的设置而中断,只是按出厂值跑。
///
/// 抓得住的变异:删掉 `DownloadTuning::from_settings` 的区间过滤 ⇒ 9_999 那条把许可数变成 64,红;
/// 把 CLI 的读数改成 `expect`/报错 ⇒ 坏值文件这几档 panic 或编译不过,红。
/// 抓不住的:`problems()` 的内容与措辞(那是 core 的 `setting` 模块,本用例只借它证明文件真的坏)。
#[test]
fn missing_or_illegal_values_fall_back_to_the_factory_defaults_silently() {
let dir = TempDir::new("fallback");
let ctx = ctx_at(&dir);
assert_eq!(
super::tuning(&ctx),
DownloadTuning::default(),
"没有 launcher.json 就该是出厂调优"
);
for (case, document) in [
(
"越界",
json!({ "download": { "concurrency": 9_999, "retry_limit": 99 } }),
),
(
"负数",
json!({ "download": { "concurrency": -4, "retry_limit": -1 } }),
),
(
"类型不对",
json!({ "download": { "concurrency": "八", "retry_limit": "很多" } }),
),
] {
write_launcher_config(&ctx, document);
// 先证明这份文件**真的**是坏的(否则「回落」只是键不存在,用例就成了空断言)。
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
assert!(
!store.problems().is_empty(),
"{case}:这份文件该被判为有问题"
);
assert_eq!(
super::tuning(&ctx),
DownloadTuning::default(),
"{case}:坏值必须回落出厂调优"
);
let engine = crate::commands::install::build_engine(&ctx);
assert_concurrency(&engine, DEFAULT_MAX_CONCURRENCY, case);
assert_eq!(
engine.config().max_attempts_per_source,
DEFAULT_MAX_ATTEMPTS_PER_SOURCE,
"{case}:尝试次数也回落出厂值"
);
}
}
+2
View File
@@ -6,6 +6,7 @@
//! 分层: //! 分层:
//! - [`cli`]:手写参数解析(不引 clap)+ 命令模型 + 帮助文案; //! - [`cli`]:手写参数解析(不引 clap)+ 命令模型 + 帮助文案;
//! - [`commands`]:每个子命令一个函数,输入 [`context::Ctx`],输出 [`output::Report`]; //! - [`commands`]:每个子命令一个函数,输入 [`context::Ctx`],输出 [`output::Report`];
//! - [`download`]:CLI 侧的下载引擎(设置 store → core 调优,`install`/`java`/`pack` 共用);
//! - [`context`]:数据根解析(三模式 / `--data-dir` 覆盖)与目录视图; //! - [`context`]:数据根解析(三模式 / `--data-dir` 覆盖)与目录视图;
//! - [`os`]:系统摘要与 D19/D21 支持级别(事实来自 core::platform::os_info,这里只做呈现); //! - [`os`]:系统摘要与 D19/D21 支持级别(事实来自 core::platform::os_info,这里只做呈现);
//! - [`output`]:人类可读 + `--json` 双格式输出; //! - [`output`]:人类可读 + `--json` 双格式输出;
@@ -19,6 +20,7 @@
pub mod cli; pub mod cli;
pub mod commands; pub mod commands;
pub mod context; pub mod context;
pub mod download;
pub mod error; pub mod error;
pub mod os; pub mod os;
pub mod output; pub mod output;
+6 -4
View File
@@ -55,6 +55,9 @@ use super::mirrors::MirrorTable;
use super::region::{decide_from, load_cached, store_cached}; use super::region::{decide_from, load_cached, store_cached};
use super::region_system::detect_signals; use super::region_system::detect_signals;
mod tuned;
pub use tuned::engine_with_tuning;
/// 登记处:缓存根 → 引擎。 /// 登记处:缓存根 → 引擎。
fn registry() -> &'static Mutex<HashMap<PathBuf, Arc<DownloadEngine>>> { fn registry() -> &'static Mutex<HashMap<PathBuf, Arc<DownloadEngine>>> {
static ENGINES: OnceLock<Mutex<HashMap<PathBuf, Arc<DownloadEngine>>>> = OnceLock::new(); static ENGINES: OnceLock<Mutex<HashMap<PathBuf, Arc<DownloadEngine>>>> = OnceLock::new();
@@ -136,10 +139,9 @@ pub fn constructions(cache_root: impl AsRef<Path>) -> usize {
.unwrap_or(0) .unwrap_or(0)
} }
/// 真实构造(**presenter 侧**唯一一处 `DownloadEngine::new` 的宿主侧装配;CLI 还有三处自建, /// 真实构造(**presenter 侧**唯一一处 `DownloadEngine::new` 的宿主装配;CLI 走 [`engine_with_tuning`])。
/// 见 `docs/download-settings.md` §3 的「哪些路径吃这套设置」)。装配参数从该数据根的 [`wanted`] 取, /// 装配参数从该数据根的 [`wanted`] 取,不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore`
/// 不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore` 容量、尝试次数 → `attempts_for()` /// 容量、尝试次数 → `attempts_for()` 上限、gh_proxy → 地区档案(`expand()` 只认档案里那个前缀)。
/// 上限、gh_proxy → 地区档案(`expand()` 只认档案里那个前缀)。值由宿主经 [`set_tuning`] 灌入。
fn build(cache_root: &Path) -> DownloadEngine { fn build(cache_root: &Path) -> DownloadEngine {
let now = now_ms(); let now = now_ms();
let tune = wanted(cache_root); let tune = wanted(cache_root);
+85
View File
@@ -0,0 +1,85 @@
//! 带调优取引擎:把「灌设置」与「取引擎」绑成一次调用(M203)。
//!
//! [`super::set_tuning`] 与 [`super::engine_for`] 是两处显式同步点,宿主少调前一步就会**静默**
//! 拿到出厂参数的引擎——CLI 的 `install` / `java` / `pack` 此前各自 `DownloadEngine::new(
//! EngineConfig::default(), …)`,用户在设置里配的并发 / 重试 / gh-proxy 一概不生效。
//! 把它们绑成一次调用,漏调就成了类型错误而不是「跑起来但用出厂值」。
//!
//! 现有 [`super::engine_for`] 的签名与行为**不动**(presenter 走 `set_tuning` + `engine_for`
//! 两步,语义相同,见 `presenter::host::settings::download::apply_tuning`)。
use std::path::Path;
use std::sync::Arc;
use super::super::config::DownloadTuning;
use super::super::engine::DownloadEngine;
use super::{engine_for, set_tuning};
/// 先灌该数据根的调优,再取它的共享引擎(调优没变时复用的是同一个引擎)。
///
/// 幂等:值没变时 `set_tuning` 一行不动,所以「每次取引擎前都调」不会白扔连接池。
#[must_use]
pub fn engine_with_tuning(
cache_root: impl AsRef<Path>,
tune: DownloadTuning,
) -> Arc<DownloadEngine> {
let root = cache_root.as_ref();
let _ = set_tuning(root, tune);
engine_for(root)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::download::config::{DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY};
use std::path::PathBuf;
fn root(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"ull-shared-tuned-{tag}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("建缓存根");
dir
}
/// **调优与取引擎必须是一步**:调 `engine_with_tuning` 就该拿到吃这套参数的引擎。
///
/// 抓得住的变异:把 `engine_with_tuning` 里的 `set_tuning(...)` 删掉(只留 `engine_for`)⇒
/// 探针停在出厂 8,红。抓不住的:设置键 → 调优那一段(在 CLI 侧,`cli/src/download/tests.rs`)。
#[test]
fn the_tuning_reaches_the_engine_it_returns() {
let dir = root("reach");
let engine =
engine_with_tuning(&dir, DownloadTuning::from_settings(Some(3), Some(2), None));
assert_eq!(engine.config().max_concurrency, 3);
assert_eq!(engine.concurrency_probe(), 3, "闸门真开了 3 张许可");
assert_eq!(engine.config().max_attempts_per_source, 2);
// 出厂调优与「没灌过」等价:不该因为这次调用就把引擎参数写坏。
let plain = engine_with_tuning(&dir, DownloadTuning::default());
assert_eq!(plain.config().max_concurrency, DEFAULT_MAX_CONCURRENCY);
assert_eq!(
plain.config().max_attempts_per_source,
DEFAULT_MAX_ATTEMPTS_PER_SOURCE
);
std::fs::remove_dir_all(&dir).ok();
}
/// 调优变了就要换引擎(`set_tuning` 的那半个承诺在这里也成立)。
///
/// 抓得住的变异:`engine_with_tuning` 只 `engine_for` 不看调优的变更 ⇒ 交回旧引擎(`ptr_eq` 红)。
#[test]
fn a_changed_tuning_yields_a_rebuilt_engine() {
let dir = root("rebuild");
let first = engine_with_tuning(&dir, DownloadTuning::from_settings(Some(2), None, None));
let same = engine_with_tuning(&dir, DownloadTuning::from_settings(Some(2), None, None));
assert!(Arc::ptr_eq(&first, &same), "同值不该重建");
let next = engine_with_tuning(&dir, DownloadTuning::from_settings(Some(5), None, None));
assert!(!Arc::ptr_eq(&first, &next), "改了并发必须重建");
assert_eq!(next.concurrency_probe(), 5);
std::fs::remove_dir_all(&dir).ok();
}
}