Merge branch 'feat/m203-clituning-cli-download-tuning-from'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
0cc1a35420
10 files changed
+449
-86
No files matched your search
@@ -19,12 +19,7 @@ mod vanilla;
|
||||
use std::sync::Arc;
|
||||
|
||||
use unlucky_core::addon::loader::LoaderKind;
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
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::download::{DownloadEngine, DownloadErrorKind};
|
||||
use unlucky_core::game::install::{InstallError, InstallReport, Registration};
|
||||
|
||||
use crate::cli::InstallArgs;
|
||||
@@ -78,27 +73,10 @@ pub(super) fn register_instance(
|
||||
.map_err(|error| CliError::user("instance_register", format!("登记实例失败:{error}")))
|
||||
}
|
||||
|
||||
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。
|
||||
///
|
||||
/// 地区判定**不做网络探测**(P2 首版;探测开关属设置层接线),
|
||||
/// 因此中文环境默认镜像优先、其它环境直连——与 D7 的"不探测时"约定一致。
|
||||
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(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
let _ = store_cached(®ion_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)),
|
||||
)
|
||||
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,装配参数来自设置(并发 / 重试 / gh-proxy,
|
||||
/// 读数口径见 [`crate::download`])——不再用裸 `EngineConfig::default()`(M203)。
|
||||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
crate::download::engine(ctx)
|
||||
}
|
||||
|
||||
/// 同名但绑定别的版本 → 冲突;同名同版本视为重装(复用实例,允许)。
|
||||
|
||||
@@ -17,12 +17,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
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::download::{DownloadEngine, RegionProfile};
|
||||
use unlucky_core::game::java::{
|
||||
self, location, vendor, InstallRequest, JavaOrigin, JavaRuntime, JdkVendor, LocationMode,
|
||||
DEFAULT_MAJOR,
|
||||
@@ -312,24 +307,10 @@ fn install_root_of(runtime: &JavaRuntime) -> Option<String> {
|
||||
.map(|dir| dir.display().to_string())
|
||||
}
|
||||
|
||||
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。
|
||||
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(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
let _ = store_cached(®ion_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)),
|
||||
)
|
||||
/// 建下载引擎:装配参数来自设置(并发 / 重试 / gh-proxy,读数口径见 [`crate::download`]),
|
||||
/// 引擎本体与 `install` / `pack` 共用同一个(core 按缓存根共享)。
|
||||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
crate::download::engine(ctx)
|
||||
}
|
||||
|
||||
/// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。
|
||||
|
||||
@@ -30,12 +30,7 @@ use unlucky_core::addon::import::{
|
||||
ImportReport, ImportRequest, ImportStatus,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
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 unlucky_core::download::DownloadEngine;
|
||||
|
||||
use crate::cli::PackImportArgs;
|
||||
use crate::context::Ctx;
|
||||
@@ -286,24 +281,10 @@ fn count_action(report: &ImportReport, action: FileAction) -> usize {
|
||||
.count()
|
||||
}
|
||||
|
||||
/// 与 `install` 共用同一套引擎构造(镜像 / 地区 / 健康度 / 内容缓存)。
|
||||
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(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
let _ = store_cached(®ion_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)),
|
||||
)
|
||||
/// 与 `install` / `java` 共用同一套引擎装配(镜像 / 地区 / 健康度 / 内容缓存),
|
||||
/// 参数来自设置:读数口径见 [`crate::download`]。
|
||||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
crate::download::engine(ctx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -37,6 +37,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
use unlucky_core::update::{default_source, Applied, CheckOutcome, Job, UpdateError, Version};
|
||||
|
||||
use crate::cli::{UpdateApplyArgs, UpdateCheckArgs};
|
||||
@@ -54,10 +55,20 @@ fn current_version() -> Result<Version, CliError> {
|
||||
.map_err(|error| CliError::internal("bad_version", format!("编译期版本号不合法:{error}")))
|
||||
}
|
||||
|
||||
/// 自更新的下载引擎:与 `install` / `java` / `pack` 同一条装配(参数来自
|
||||
/// [`crate::download`],设置读数与落点在那边)。
|
||||
///
|
||||
/// 自更新只下**一个** artifact(`update::github` 的 `fetch_artifact` → 单次 `engine.download`),
|
||||
/// 所以并发的差别在这条路径上不可观测;真正随设置变的是**每源重试次数**与**gh-proxy 前缀**——
|
||||
/// 那正是「设置说真话」的两维。单独留这个接缝是为了让用例能逐命令打点(变异时只有它红)。
|
||||
pub(crate) fn engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
super::install::build_engine(ctx)
|
||||
}
|
||||
|
||||
/// 造一个内核 job(源 = GitHub / `ULL_UPDATE_DIR`,暂存 = 数据根缓存,目标 = 本进程 exe)。
|
||||
fn job(ctx: &Ctx, dest: Option<PathBuf>) -> Job {
|
||||
let engine = super::install::build_engine(ctx);
|
||||
let job = Job::for_process(default_source(Arc::new(engine)), &ctx.layout().cache());
|
||||
let engine = engine(ctx);
|
||||
let job = Job::for_process(default_source(engine), &ctx.layout().cache());
|
||||
match dest {
|
||||
Some(dest) => job.with_dest(dest),
|
||||
None => job,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//! 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 记着这三条 ❌)。第四条路径 `update`(自更新)复用 `install`
|
||||
//! 的装配,也随之吃设置;它只下一个 artifact,所以并发在那条路径上不可观测。
|
||||
//!
|
||||
//! 现在四条一律经 [`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;
|
||||
@@ -0,0 +1,266 @@
|
||||
//! [`super`] 的单测(M203):`launcher.json` 的 `download.*` 真的进到四条命令拿到的引擎里
|
||||
//! (`install` / `java` / `pack` / `update`——`update` 复用 `install` 的装配,自更新只下一个
|
||||
//! artifact,所以并发在那条路径上不可观测,随设置变的是每源重试次数与 gh-proxy 前缀)。
|
||||
//!
|
||||
//! 断言读的是**引擎的真实字段**——`config().max_concurrency` 与 `Semaphore` 的
|
||||
//! `available_permits()`(`concurrency_probe`)——不是「函数被调用过」:后者在回落写错、
|
||||
//! 或者命令又自建了一个裸引擎时照样绿。gh-proxy 那一条断言的是**候选链**(真行为),不是配置值。
|
||||
|
||||
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, KEY_CONCURRENCY,
|
||||
};
|
||||
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
||||
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`——落点用 [`Ctx::setting_path_for`](`ull config set` 用的那个表达式),
|
||||
/// **不是**实现读侧用的 `ctx.launcher_config()`。
|
||||
///
|
||||
/// 两个表达式今天指向同一份文件(`download.*` 不是数据根键 ⇒ 回落 `launcher_config()`),但写侧
|
||||
/// 走「用户改配置那条路」的 API,实现哪天改读别的文件时这条用例会红,而不是跟着一起漂移。
|
||||
fn write_launcher_config(ctx: &Ctx, document: Value) -> PathBuf {
|
||||
let path = ctx.setting_path_for(KEY_CONCURRENCY);
|
||||
std::fs::create_dir_all(path.parent().expect("配置目录")).expect("建配置目录");
|
||||
std::fs::write(&path, serde_json::to_vec(&document).expect("序列化"))
|
||||
.expect("写 launcher.json");
|
||||
path
|
||||
}
|
||||
|
||||
/// 「引擎真的按这个并发跑」:配置字段 + 闸门许可数,两处都读真实值。
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
/// **`update` 吃设置**(第 4 条路径,`commands/update.rs::engine`):自更新也只拿出厂值是不行的。
|
||||
///
|
||||
/// 这条路径上「并发」实际不可观测(自更新只下**一个** artifact,闸门开 1 还是 8 都只有一条连接),
|
||||
/// 但引擎参数是同一处装配出来的,用并发打点最省事;真正随设置变的是每源重试次数与 gh-proxy 前缀。
|
||||
/// 抓得住的变异:`commands::update::engine` 换回裸 `EngineConfig::default()` ⇒ 4 ≠ 8 处红,另五条绿。
|
||||
#[test]
|
||||
fn update_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
let dir = TempDir::new("update");
|
||||
let ctx = ctx_at(&dir);
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 4 } }));
|
||||
assert_concurrency(&crate::commands::update::engine(&ctx), 4, "concurrency=4");
|
||||
}
|
||||
|
||||
/// **`gh_proxy` 的 CLI 侧闭环**:手写 `"off"` ⇒ 候选链里真的没有 gh-proxy(读的是 `plan()`,不是配置值)。
|
||||
///
|
||||
/// 两段式:先在镜像优先的地区档案下确认**出厂**(空串 = 跟随内置)确实生成 gh-proxy 候选,再把它
|
||||
/// 改成 `off` 确认候选消失——只断言后者的话,「本来就没有候选」会让用例白绿。
|
||||
/// 抓得住的变异:`download::tuning` 不读 `KEY_GH_PROXY`(传 `None`)⇒ 第二段仍有 gh-proxy 候选,红;
|
||||
/// 把空串当成关(`gh_proxy_override("")` 返回 `Off`)⇒ 第一段没有候选,红(回落用例的
|
||||
/// `tuning == default` 同时红——两处都该红,实测如此)。
|
||||
/// 抓不住的:真实网络下这个前缀能不能通(离线只验候选链的**生成**)。
|
||||
#[test]
|
||||
fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::region::{store_cached, RegionProfile};
|
||||
|
||||
let dir = TempDir::new("ghproxy");
|
||||
let ctx = ctx_at(&dir);
|
||||
// gh-proxy 只在「镜像优先」的地区档案下生成候选:先把该数据根的 region.json 钉住。
|
||||
let cache_root = ctx.layout().cache();
|
||||
store_cached(
|
||||
&cache_root.join("region.json"),
|
||||
&RegionProfile::mirror_first(now_ms()),
|
||||
)
|
||||
.expect("地区档案");
|
||||
let url = "https://github.com/o/r/releases/download/v1/a.jar";
|
||||
let chain = |ctx: &Ctx| -> Vec<String> {
|
||||
let request =
|
||||
DownloadRequest::new(ResourceKind::Other, url, ctx.layout().cache().join("a.jar"));
|
||||
crate::download::engine(ctx)
|
||||
.plan(&request, 0)
|
||||
.into_iter()
|
||||
.map(|candidate| candidate.url)
|
||||
.collect()
|
||||
};
|
||||
let has_gh_proxy = |chain: &[String]| chain.iter().any(|url| url.contains("gh-proxy"));
|
||||
|
||||
// 出厂(未配置):空串 = 跟随内置默认 ⇒ 有 gh-proxy 候选。
|
||||
assert!(has_gh_proxy(&chain(&ctx)), "出厂该套内置前缀");
|
||||
// 写 off:候选里不该再有 gh-proxy(其余候选仍在)。
|
||||
write_launcher_config(&ctx, json!({ "download": { "gh_proxy": "off" } }));
|
||||
let off = chain(&ctx);
|
||||
assert!(!has_gh_proxy(&off), "off 档还有 gh-proxy 候选:{off:?}");
|
||||
assert!(!off.is_empty(), "关掉加速不等于没有候选:{off:?}");
|
||||
}
|
||||
|
||||
/// **反向断言**:键缺失 / 越界 / 负数 / 类型不对 ⇒ 回落出厂值,而且回落是**静默的**。
|
||||
///
|
||||
/// 「静默」在这里是结构性的:`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": "很多" } }),
|
||||
),
|
||||
] {
|
||||
let written = write_launcher_config(&ctx, document);
|
||||
// 先证明这份文件**真的**是坏的(否则「回落」只是键不存在,用例就成了空断言)。
|
||||
let store = SettingStore::load(&written, 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}:尝试次数也回落出厂值"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//! 分层:
|
||||
//! - [`cli`]:手写参数解析(不引 clap)+ 命令模型 + 帮助文案;
|
||||
//! - [`commands`]:每个子命令一个函数,输入 [`context::Ctx`],输出 [`output::Report`];
|
||||
//! - [`download`]:CLI 侧的下载引擎(设置 store → core 调优,`install`/`java`/`pack` 共用);
|
||||
//! - [`context`]:数据根解析(三模式 / `--data-dir` 覆盖)与目录视图;
|
||||
//! - [`os`]:系统摘要与 D19/D21 支持级别(事实来自 core::platform::os_info,这里只做呈现);
|
||||
//! - [`output`]:人类可读 + `--json` 双格式输出;
|
||||
@@ -19,6 +20,7 @@
|
||||
pub mod cli;
|
||||
pub mod commands;
|
||||
pub mod context;
|
||||
pub mod download;
|
||||
pub mod error;
|
||||
pub mod os;
|
||||
pub mod output;
|
||||
|
||||
@@ -33,11 +33,11 @@ pub const MAX_ATTEMPTS_PER_SOURCE: u32 = 10;
|
||||
pub const KEY_CONCURRENCY: &str = "download.concurrency";
|
||||
/// 见 [`KEY_CONCURRENCY`]。
|
||||
pub const KEY_RETRY_LIMIT: &str = "download.retry_limit";
|
||||
/// gh-proxy 前缀覆盖键(三态语义见 [`gh_proxy_override`])。
|
||||
///
|
||||
/// **暂未登记**:由 M197 在 `setting::keys::proxy` 登记,尚未合入 dev;宿主读它得到 `None` ⇒
|
||||
/// [`GhProxyOverride::Builtin`](行为正确)。**M197 合入后这条断言应升级为 schema 断言**——现在
|
||||
/// 只断言常量本身,别把这条弱断言当成可以随手删的。
|
||||
/// gh-proxy 前缀覆盖键(三态语义见 [`gh_proxy_override`])。**已登记**:`setting::keys::proxy` 的
|
||||
/// `specs()` 里有它(文本 / 出厂空串),presenter 与 CLI 的宿主都按这个常量读,读不到 ⇒
|
||||
/// [`GhProxyOverride::Builtin`]。**弱在哪(别当死键删)**:本文件没有断言把它与 schema 登记绑起来,
|
||||
/// 常量值被改名或写错时这里不红,行为只会在宿主侧静默退化成 `Builtin`(用户填的 `off` 被忽略);
|
||||
/// 拦住行为退化的是 `empty_gh_proxy_means_follow_builtin_not_off` 与 `shared` 的「调优 → 候选链」。
|
||||
pub const KEY_GH_PROXY: &str = "download.gh_proxy";
|
||||
|
||||
/// 引擎配置。
|
||||
|
||||
@@ -55,6 +55,9 @@ use super::mirrors::MirrorTable;
|
||||
use super::region::{decide_from, load_cached, store_cached};
|
||||
use super::region_system::detect_signals;
|
||||
|
||||
mod tuned;
|
||||
pub use tuned::engine_with_tuning;
|
||||
|
||||
/// 登记处:缓存根 → 引擎。
|
||||
fn registry() -> &'static Mutex<HashMap<PathBuf, Arc<DownloadEngine>>> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 真实构造(**presenter 侧**唯一一处 `DownloadEngine::new` 的宿主侧装配;CLI 还有三处自建,
|
||||
/// 见 `docs/download-settings.md` §3 的「哪些路径吃这套设置」)。装配参数从该数据根的 [`wanted`] 取,
|
||||
/// 不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore` 容量、尝试次数 → `attempts_for()`
|
||||
/// 上限、gh_proxy → 地区档案(`expand()` 只认档案里那个前缀)。值由宿主经 [`set_tuning`] 灌入。
|
||||
/// 真实构造(**presenter 侧**唯一一处 `DownloadEngine::new` 的宿主装配;CLI 走 [`engine_with_tuning`])。
|
||||
/// 装配参数从该数据根的 [`wanted`] 取,不用裸 `EngineConfig::default()`(M193):并发 → `Semaphore`
|
||||
/// 容量、尝试次数 → `attempts_for()` 上限、gh_proxy → 地区档案(`expand()` 只认档案里那个前缀)。
|
||||
fn build(cache_root: &Path) -> DownloadEngine {
|
||||
let now = now_ms();
|
||||
let tune = wanted(cache_root);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user