fix(download): M294 r1 —— JDK 下载页与 CLI 也接上源选择,文档同步
复审 p1:发布版 GUI 的「下载 JDK」页是第二处 DownloadEngine::new(要 with_progress 拿走引擎),此前只读了 gh_proxy,region / mirror_preference 到不了那里;报告却把 GUI 写成「已接」。复审 p2:CLI 四条命令同样不认这两个键 —— 同一份 launcher.json 在 GUI 与 CLI 上表现不同,等于把「设置项说假话」挪到 CLI 侧。 - app-iced downloads/engine.rs:新增 SourceSelection(只读键、不翻译)+ engine_with(.., &SourceSelection),在 gh-proxy 三态之前叠 DownloadTuning::with_source_selection(..).apply_source_selection(..) - Request 加 source_selection 字段,构造点(app/update/settings/java/downloads.rs) 用 SourceSelection::of_config(&config) 显式传值(不从缓存根反推配置路径: 反推失败会静默按 auto 收场,正是本 mission 要消灭的形状) - cli download.rs 的 tuning():链 .with_source_selection(..),四条命令共用这一处装配 - core 的 summary():把「地区 / 镜像偏好」两档印进 CLI 那行生效值(CLI 既然读了, 少印两项就是新的半真话) - shared.rs 注释说清「共享引擎」与本文件的第二处装配点;app-iced 模块头补对照表 - 新判据:JDK 下载页与 CLI 各一条断候选源顺序的用例,均配变异验证(换回裸 EngineConfig::default() / 裸 tuning 即红);补一条 region=cn + gh_proxy=off 的 叠加次序守卫(复审 P3-2) - docs/download-settings.md:§1 表格两行、§5 拆「已接 / 未接」、新增 §3.1.1 覆盖面 表、§6 补 M14-M16 三条变异、§7 补「镜像可达性没验」 连带(不加就编不过,已报备 tower):downloads/tests.rs 的 Request 字面量 +1 行、 downloads/tests/gh_proxy.rs 两处 engine_with 调用点补第四参。
This commit is contained in:
1 parent
0f64acbd05
commit
69e3e198bf
11 files changed
+395
-36
No files matched your search
@@ -54,7 +54,7 @@ mod engine;
|
||||
mod recommend;
|
||||
mod refusal;
|
||||
|
||||
pub use engine::{gh_proxy_of_config, GhProxy};
|
||||
pub use engine::{gh_proxy_of_config, GhProxy, SourceSelection};
|
||||
pub use recommend::recommend;
|
||||
pub use refusal::Refusal;
|
||||
|
||||
@@ -211,6 +211,11 @@ pub struct Request {
|
||||
pub priority: Vec<JdkVendor>,
|
||||
/// `download.gh_proxy`(`Builtin` = 跟随内置默认;`Off` = 关;`Prefix` = 覆盖前缀)
|
||||
pub gh_proxy: GhProxy,
|
||||
/// `download.region` / `download.mirror_preference` 的原文(M294;两个 `None` = 都没设 = 按探测)
|
||||
///
|
||||
/// 由调用方从**同一份** `launcher.json` 读出后显式传进来([`SourceSelection::of_config`]):
|
||||
/// 装配点不再自己去猜配置在哪。翻译(`auto` 回落/优先级)仍只有 core 那一处。
|
||||
pub source_selection: SourceSelection,
|
||||
}
|
||||
|
||||
/// 作业槽:快照 + 「线程还活着」。
|
||||
@@ -272,7 +277,12 @@ impl Slot {
|
||||
feed.apply(event);
|
||||
}
|
||||
});
|
||||
let engine = engine::engine_with(&request.cache_root, emitter, &request.gh_proxy);
|
||||
let engine = engine::engine_with(
|
||||
&request.cache_root,
|
||||
emitter,
|
||||
&request.gh_proxy,
|
||||
&request.source_selection,
|
||||
);
|
||||
let install_request = InstallRequest::new(request.major).with_priority(request.priority);
|
||||
let result = block_on(install(&engine, &request.install_root, install_request));
|
||||
// 引擎丢掉 ⇒ 发射器丢掉 ⇒ 抽干线程读到 `None` 自然退出。
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
//! 带进度发射器的下载引擎(M197):一次 JDK 安装一个实例 + `download.gh_proxy` 的真消费者。
|
||||
//! 带进度发射器的下载引擎(M197):一次 JDK 安装一个实例 + `download.*` 的真消费者。
|
||||
//!
|
||||
//! 从 `downloads.rs` 拆出来(行数 + 口径独立:这里只说「为什么不用共享引擎」)。
|
||||
//! 装配口径与 `core::download::shared::build` 一致,只多挂一个 `ProgressEmitter`。
|
||||
//!
|
||||
//! ## 与 `core::download::shared::build` 的关系(别读成「一样」)
|
||||
//!
|
||||
//! 一个数据根上**有两处** `DownloadEngine::new`:共享登记处(`shared::build`,GUI 主路径与 CLI
|
||||
//! 都走它)和这里(JDK 下载页,因为 `with_progress` 要**拿走**引擎)。差别是实打实的:
|
||||
//!
|
||||
//! | | `shared::build` | 这里 |
|
||||
//! |---|---|---|
|
||||
//! | 缓存 / 健康度 | 该数据根 | 同一份(盘上共用) |
|
||||
//! | 设置调优(并发 / 重试 / 三段超时) | `wanted(cache_root)`(M193/M292/M289) | **`EngineConfig::default()`** —— 未接,已知差 |
|
||||
//! | 源选择(`region` / `mirror_preference`) | `apply_source_selection`(M294) | 同一入口(M294 r1) |
|
||||
//! | gh-proxy 三态 | `DownloadTuning::apply_gh_proxy` | 本文件的 [`GhProxy`](M197) |
|
||||
//! | 进度事件 | `ProgressEmitter::silent()` | 这次作业的 emitter |
|
||||
//!
|
||||
//! 第一行「调优」是**没接**的差:JDK 页不理会 `download.concurrency` / 超时键。要接就把它并进
|
||||
//! `DownloadTuning` 一起用 `wanted(cache_root)`——那是另一个决定面,不在 M294 里顺手做。
|
||||
//! 「源选择」那一行本轮接上了(r1 复审 p1:设置页改「地区/镜像偏好」在这条路径上没生效),
|
||||
//! 并且走的是**与共享引擎同一个翻译层**(`DownloadTuning::with_source_selection`),宿主里没有
|
||||
//! 第二份字符串匹配。
|
||||
//!
|
||||
//! ## gh-proxy 的取值链(任务书那句「(默认值可改)」的落点)
|
||||
//!
|
||||
@@ -24,8 +42,10 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::config::KEY_GH_PROXY;
|
||||
use unlucky_core::download::region::{decide_from, load_cached};
|
||||
use unlucky_core::download::config::{
|
||||
DownloadTuning, KEY_GH_PROXY, KEY_MIRROR_PREFERENCE, KEY_REGION,
|
||||
};
|
||||
use unlucky_core::download::region::{decide_from, load_cached, RegionProfile};
|
||||
use unlucky_core::download::region_system::detect_signals;
|
||||
use unlucky_core::download::{
|
||||
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable, ProgressEmitter,
|
||||
@@ -76,7 +96,46 @@ pub fn gh_proxy_of_config(config: &Path) -> GhProxy {
|
||||
gh_proxy_from(store.doc().effective_str(KEY_GH_PROXY).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 带进度发射器的引擎(装配口径与 `download::shared::build` 一致,只多挂一个 emitter + gh-proxy 覆盖)。
|
||||
/// 一次 JDK 下载要用的两个「源选择」键的**原文**(`download.region` / `download.mirror_preference`)。
|
||||
///
|
||||
/// **显式传值**:由按钮回调(`app/update/settings/java/downloads.rs`)从同一份 `launcher.json` 读出、
|
||||
/// 挂进 [`Request`](crate::pages::settings::java::downloads::Request) 随作业传到这里,「设置从哪来」
|
||||
/// 在调用处一眼可读。不从缓存根反推配置路径——反推失败就静默按 `auto` 收场,正是这条 mission 要
|
||||
/// 消灭的那种「设置被无声忽略」。
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct SourceSelection {
|
||||
/// `download.region` 原文(`None` = 键不存在 / 不是字符串)。
|
||||
pub region: Option<String>,
|
||||
/// `download.mirror_preference` 原文(同上)。
|
||||
pub mirror_preference: Option<String>,
|
||||
}
|
||||
|
||||
impl SourceSelection {
|
||||
/// 从 `launcher.json` 把两个键**读出来**(文件不存在 / 键没写 ⇒ `effective_str` 给 schema 默认
|
||||
/// 值 `"auto"`;非字符串值才是 `None`。两者在 core 那里同义,都落成 `Auto`)。
|
||||
///
|
||||
/// **这里不做默认值回落、也不做取值校验**:`auto` 的语义、大小写与空白的归一、未知值怎么退,
|
||||
/// 全部由 [`DownloadTuning::with_source_selection`] 负责(全仓唯一一处)。在这个函数里再加一层
|
||||
/// 判断,就是「两份口径」的种子——GUI 认一套、CLI 认另一套。
|
||||
#[must_use]
|
||||
pub fn of_config(config: &Path) -> Self {
|
||||
let store = SettingStore::load(config, launcher_schema());
|
||||
let doc = store.doc();
|
||||
Self {
|
||||
region: doc.effective_str(KEY_REGION).map(str::to_string),
|
||||
mirror_preference: doc.effective_str(KEY_MIRROR_PREFERENCE).map(str::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
/// 叠到地区档案上:显式设置压过探测结论(`gh_proxy` 留 `Builtin`,前缀仍由调用点那三态决定)。
|
||||
fn apply(&self, profile: RegionProfile) -> RegionProfile {
|
||||
DownloadTuning::default()
|
||||
.with_source_selection(self.region.as_deref(), self.mirror_preference.as_deref())
|
||||
.apply_source_selection(profile)
|
||||
}
|
||||
}
|
||||
|
||||
/// 带进度发射器的引擎(装配口径见模块头的对照表,**不等于** `shared::build`)。
|
||||
///
|
||||
/// 为什么不再走 `shared::engine_for`:那个登记处交出的是 `Arc<DownloadEngine>`,而挂进度要
|
||||
/// 拿走引擎(`with_progress(self)`)。要让共享引擎带「按作业」的观察者,得改引擎内部的
|
||||
@@ -87,6 +146,7 @@ pub(super) fn engine_with(
|
||||
cache_root: &Path,
|
||||
progress: ProgressEmitter,
|
||||
gh_proxy: &GhProxy,
|
||||
selection: &SourceSelection,
|
||||
) -> DownloadEngine {
|
||||
let now = now_ms();
|
||||
let profile = load_cached(&cache_root.join("region.json"), now).unwrap_or_else(|| {
|
||||
@@ -94,6 +154,10 @@ pub(super) fn engine_with(
|
||||
let _ = std::fs::create_dir_all(cache_root);
|
||||
profile
|
||||
});
|
||||
// 显式地区 / 镜像偏好先叠(M294):`DownloadTuning::default()` 的 gh_proxy 是 `Builtin`
|
||||
// (恒等),所以这一步只动「谁在前」;前缀仍由下面那三态决定——那是 JDK 页 `off` 档的唯一
|
||||
// 真相源,不在这里再翻译一次。
|
||||
let profile = selection.apply(profile);
|
||||
let profile = match gh_proxy {
|
||||
GhProxy::Builtin => profile,
|
||||
GhProxy::Off => profile.with_gh_proxy(None),
|
||||
@@ -108,3 +172,128 @@ pub(super) fn engine_with(
|
||||
)
|
||||
.with_progress(progress)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! JDK 下载页这个**独立装配点**的守门(M294 r1 复审 P1-1)。
|
||||
//!
|
||||
//! 不进 `downloads/tests.rs`:那一族守的是作业/快照,而这里守的是装配点本身,测试就在被守的
|
||||
//! 函数旁边(`engine_with` 是 `pub(super)`,同文件内可见)。
|
||||
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
use unlucky_core::download::region::{store_cached, RegionProfile};
|
||||
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
||||
|
||||
const LIBRARIES_URL: &str =
|
||||
"https://libraries.minecraft.net/com/google/guava/guava/32.0.0/guava-32.0.0.jar";
|
||||
|
||||
/// 一次 JDK 下载的临时数据根(`<dir>/cache` + `<dir>/config`,与 `prefs_store` 的布局一致)。
|
||||
fn root(tag: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("ull-jdk-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("cache")).expect("cache");
|
||||
std::fs::create_dir_all(dir.join("config")).expect("config");
|
||||
dir
|
||||
}
|
||||
|
||||
/// 这个数据根的 `launcher.json`(与 `prefs_store` 的布局一致:`<home>/config/launcher.json`)。
|
||||
fn config_of(dir: &Path) -> PathBuf {
|
||||
dir.join("config").join("launcher.json")
|
||||
}
|
||||
|
||||
fn write_config(dir: &Path, key: &str, value: &str) {
|
||||
let mut store = SettingStore::load(config_of(dir), launcher_schema());
|
||||
store
|
||||
.set(key, value.to_string().into())
|
||||
.unwrap_or_else(|err| panic!("schema 认识 {key}:{err}"));
|
||||
store.save().expect("写 launcher.json");
|
||||
}
|
||||
|
||||
fn clear_config(dir: &Path) {
|
||||
std::fs::write(config_of(dir), b"{}").expect("清空 launcher.json");
|
||||
}
|
||||
|
||||
fn plan_sources(engine: &DownloadEngine, dest: &Path) -> Vec<String> {
|
||||
let request = DownloadRequest::new(
|
||||
ResourceKind::Library,
|
||||
LIBRARIES_URL.to_string(),
|
||||
dest.to_path_buf(),
|
||||
);
|
||||
engine
|
||||
.plan(&request, 0)
|
||||
.into_iter()
|
||||
.map(|candidate| candidate.source)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// **P1-1 判据**:`download.region` / `download.mirror_preference` 改得动 JDK 下载页的候选顺序。
|
||||
///
|
||||
/// 两段都让设置与 `region.json` 的探测结论**相反**,所以「装配点没读设置」时断言必红
|
||||
/// (未接线时两段的第一条都还是探测值 `bmclapi` / `official`)。
|
||||
///
|
||||
/// 变异证明:删掉 `engine_with` 里那句 `apply_source_selection`(换回裸档案)⇒ 两段顺序各自红。
|
||||
/// 抓不住:真机上下载 JDK 时实际命中哪个镜像(离线,没有网络证据)。
|
||||
#[test]
|
||||
fn the_source_selection_settings_reach_the_jdk_download_engine() {
|
||||
let dir = root("source-selection");
|
||||
let cache = dir.join("cache");
|
||||
let now = now_ms();
|
||||
|
||||
// 探测=镜像优先,设置 official ⇒ 官方第一条。
|
||||
store_cached(
|
||||
&cache.join("region.json"),
|
||||
&RegionProfile::mirror_first(now),
|
||||
)
|
||||
.expect("档案");
|
||||
write_config(&dir, KEY_MIRROR_PREFERENCE, "official");
|
||||
let selection = SourceSelection::of_config(&config_of(&dir));
|
||||
let engine = engine_with(
|
||||
&cache,
|
||||
ProgressEmitter::silent(),
|
||||
&GhProxy::Builtin,
|
||||
&selection,
|
||||
);
|
||||
assert_eq!(
|
||||
plan_sources(&engine, &dir.join("a.jar")),
|
||||
["official", "bmclapi"],
|
||||
"JDK 下载页也必须认 mirror_preference"
|
||||
);
|
||||
|
||||
// 探测=直连,设置 cn ⇒ 镜像第一条(先清掉上一段的 official:它优先级更高)。
|
||||
store_cached(&cache.join("region.json"), &RegionProfile::direct(now)).expect("档案");
|
||||
clear_config(&dir);
|
||||
write_config(&dir, KEY_REGION, "cn");
|
||||
let selection = SourceSelection::of_config(&config_of(&dir));
|
||||
let engine = engine_with(
|
||||
&cache,
|
||||
ProgressEmitter::silent(),
|
||||
&GhProxy::Builtin,
|
||||
&selection,
|
||||
);
|
||||
assert_eq!(
|
||||
plan_sources(&engine, &dir.join("a.jar")),
|
||||
["bmclapi", "official"],
|
||||
"JDK 下载页也必须认 region"
|
||||
);
|
||||
|
||||
// 不设 ⇒ 回到探测结论(这一条与 M294 之前逐值相同)。清空后 `effective_str` 给的是
|
||||
// schema 默认值 `auto`(不是 `None`)——两者在 core 那里同义,都落成 `Auto`。
|
||||
clear_config(&dir);
|
||||
let selection = SourceSelection::of_config(&config_of(&dir));
|
||||
assert_eq!(selection.region.as_deref(), Some("auto"), "{selection:?}");
|
||||
let engine = engine_with(
|
||||
&cache,
|
||||
ProgressEmitter::silent(),
|
||||
&GhProxy::Builtin,
|
||||
&selection,
|
||||
);
|
||||
assert_eq!(
|
||||
plan_sources(&engine, &dir.join("a.jar")),
|
||||
["official", "bmclapi"],
|
||||
"不设 = 探测结论(直连档案)"
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,7 @@ fn the_real_download_installs_a_usable_jdk() {
|
||||
install_root: JavaInstallRoot::custom(dir.join("jdks")),
|
||||
priority: Vec::new(),
|
||||
gh_proxy: super::GhProxy::Builtin,
|
||||
source_selection: super::SourceSelection::default(),
|
||||
}));
|
||||
// 失败/panic 也要清理:这条用例会真下 ~180 MB,留在系统 temp 里是最贵的垃圾。
|
||||
struct Cleanup(std::path::PathBuf);
|
||||
|
||||
@@ -39,7 +39,7 @@ fn the_gh_proxy_setting_maps_to_three_states() {
|
||||
#[test]
|
||||
fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
||||
use crate::pages::settings::java::downloads::engine::engine_with;
|
||||
use crate::pages::settings::java::downloads::{gh_proxy_of_config, GhProxy};
|
||||
use crate::pages::settings::java::downloads::{gh_proxy_of_config, GhProxy, SourceSelection};
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::region::{store_cached, RegionProfile};
|
||||
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
||||
@@ -79,6 +79,8 @@ fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
||||
&cache,
|
||||
ProgressEmitter::silent(),
|
||||
&gh_proxy_of_config(&config),
|
||||
// M294:这条用例只守 gh-proxy,两个源选择键留空(= 跟随 region.json 的判定)。
|
||||
&SourceSelection::default(),
|
||||
);
|
||||
// ② 档案被覆盖
|
||||
assert_eq!(
|
||||
@@ -104,7 +106,12 @@ fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
||||
);
|
||||
|
||||
// 关掉那一档:一个代理候选都不生成(同一份 region.json,只换设置值)。
|
||||
let off = engine_with(&cache, ProgressEmitter::silent(), &GhProxy::Off);
|
||||
let off = engine_with(
|
||||
&cache,
|
||||
ProgressEmitter::silent(),
|
||||
&GhProxy::Off,
|
||||
&SourceSelection::default(),
|
||||
);
|
||||
assert_eq!(off.region().gh_proxy, None, "off 必须把前缀清掉");
|
||||
let plan = off.plan(&request, 0);
|
||||
assert!(
|
||||
|
||||
Reference in new issue
Block a user