复审 P2-1(采「修法 1」,不是只改措辞):`shared::forget` 只让登记处不再持有 那一份,换不掉自更新 `Factory` 里常驻的 Arc —— 于是「重新检测地区」对更新路径 不生效,直到重启。现在设置动作会把 Factory 一起重建。 - presenter/update_state: `Runtime::rebuild_factory` + `CoreHost::refresh_update_factory`; 只换 factory,不动 view/inflight(在飞的 Job 派发时已克隆走装配,重建不打断它) - presenter/dispatch: `SetSetting` 成功且键是「重新检测地区」时调 refresh - presenter/engine: `DETECT_REGION_ACTION` 常量(设置页行 id / set 分派 / dispatch 判断共用);放这里而不是 settings.rs 是因为后者已到 400 行红线 - core/shared: 文档改准 —— forget 的范围是「登记处不再持有」,长命持有者必须自己重建; 记账从 `engine_for` 移到 `DownloadEngine::new` 这个卡口(分两把锁,避免自锁), 于是 `constructions` 连绕过登记处另造引擎的调用点也统得住 守卫(都做了变异实证): - update_tests::detect_region_rebuilds_the_update_factory_engine:走真实命令路径 `AppCommand::SetSetting` → 断言 Factory 换了引擎且指向登记处当前实例;变异(删掉 dispatch 里那句 refresh)→ 红在 Arc::ptr_eq - update_tests::rebuilding_the_factory_does_not_touch_a_running_job:重建不清 inflight、不改快照,在飞 Job 仍能跑完 - addon/http::tests::three_host_request_paths_share_one_engine:连走三条真实宿主路径 (Modrinth 元数据 GET / CF 带 header GET / Java 页渲染)后 constructions == 1; 变异(让 get_via_engine 自己造引擎)→ 红,left:2 right:1 顺带修:settings.rs 在上一提交被推到 403 行、engine.rs 401 行,两者都超 guards.yml 的 file-size 红线(>400 直接失败)。现在 settings.rs 400 / engine.rs 399, 全仓 `*.rs` 复跑「>400」扫描无输出。 验证:core lib 608 过、5 个 download 集成 target 42 过、presenter lib 270 过、 clippy --all-targets 干净、fmt 干净。真站点仍未打(见报告「没验的部分」)。
198 lines
7.9 KiB
Rust
198 lines
7.9 KiB
Rust
//! 下载引擎与 Java 落点的装配(versions / install / java 三个任务共用)。
|
||
//!
|
||
//! **引擎是进程级的、按数据根共享的**(M165):本模块不再自己 `DownloadEngine::new`,而是向
|
||
//! [`unlucky_core::download::shared`] 要那个数据根上已经存在的引擎。于是连接池、信号量限流、
|
||
//! 镜像健康度、内容寻址缓存都跨请求累积,而不是每次请求清零(逐项代价与边界见那个模块的文档)。
|
||
//!
|
||
//! 与 CLI `install::build_engine` 同源:内容寻址缓存落 `.ULL/cache/`,地区判定走
|
||
//! 24h 缓存 + 环境信号(**不做网络探测**,与 D7 的「不探测时」约定一致);区别只在生命周期
|
||
//! ——CLI 一条命令一个进程,建一次就够;GUI 进程要在成百上千次请求之间复用同一份。
|
||
|
||
use std::path::PathBuf;
|
||
use std::sync::Arc;
|
||
|
||
use unlucky_core::download::shared;
|
||
use unlucky_core::download::DownloadEngine;
|
||
use unlucky_core::game::java::vendor::JdkVendor;
|
||
use unlucky_core::game::java::{self, JavaInstallRoot};
|
||
use unlucky_core::platform::paths::ProcessEnv;
|
||
|
||
use super::env::HostEnv;
|
||
|
||
/// 取该数据根上**共享**的下载引擎(缓存/健康度/地区判定都在数据根里)。
|
||
///
|
||
/// 同一个数据根无论被请求多少次,都是同一个引擎;不同数据根各一份(缓存/健康度不串味)。
|
||
#[must_use]
|
||
pub fn engine(env: &HostEnv) -> Arc<DownloadEngine> {
|
||
shared::engine_for(env.layout.cache())
|
||
}
|
||
|
||
/// 让这个数据根的共享引擎失效:下一次 [`engine`] 重新构造(重读地区判定/健康度/缓存索引)。
|
||
///
|
||
/// 在飞请求手里的 `Arc` 不受影响。设置页「重新检测地区」清掉 `region.json` 之后必须调它,
|
||
/// 否则那句话(「下次下载重新判定」)在进程级引擎下就不成立了。
|
||
pub fn forget(env: &HostEnv) {
|
||
shared::forget(env.layout.cache());
|
||
}
|
||
|
||
/// 是否国内网络(决定默认发行版顺序与镜像优先级)。
|
||
pub fn domestic(env: &HostEnv) -> bool {
|
||
engine(env).region().is_domestic()
|
||
}
|
||
|
||
/// 「重新检测地区」这个动作键(设置页的行 id、`settings::set` 的分派、`dispatch` 里「要不要重建
|
||
/// 自更新 Factory」的判断共用一个真相源)。
|
||
///
|
||
/// 动作不是普通设置:它要清 `region.json`、让 [`forget`] 失效共享引擎,并让自更新的 `Factory`
|
||
/// 换引擎(M165 复审 P2-1)——所以三处都得认出它。常量放这里(引擎/地区这一侧),不放已经顶到
|
||
/// 400 行红线的 `settings.rs`。
|
||
pub(super) const DETECT_REGION_ACTION: &str = "download.action.detect_region";
|
||
|
||
/// 设置里的 `java.vendorPriority`(空 = 按地区默认序)。
|
||
pub fn java_priority(env: &HostEnv) -> Vec<JdkVendor> {
|
||
env.store()
|
||
.effective("java.vendorPriority")
|
||
.and_then(serde_json::Value::as_array)
|
||
.map(|items| {
|
||
let texts: Vec<String> = items
|
||
.iter()
|
||
.filter_map(serde_json::Value::as_str)
|
||
.map(str::to_string)
|
||
.collect();
|
||
unlucky_core::game::java::vendor::parse_priority(&texts)
|
||
})
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// 设置里的 `java.installRoot`(空 = 按数据目录模式派生)。
|
||
pub fn java_install_override(env: &HostEnv) -> Option<PathBuf> {
|
||
env.store()
|
||
.doc()
|
||
.effective_str("java.installRoot")
|
||
.map(str::trim)
|
||
.filter(|text| !text.is_empty())
|
||
.map(PathBuf::from)
|
||
}
|
||
|
||
/// 托管 JDK 落点(解析失败 → 用户可读文案)。
|
||
pub fn java_install_root(env: &HostEnv) -> Result<JavaInstallRoot, String> {
|
||
let override_root = java_install_override(env);
|
||
JavaInstallRoot::resolve(
|
||
env.mode(),
|
||
&env.layout,
|
||
override_root.as_deref(),
|
||
env.platform,
|
||
&ProcessEnv,
|
||
)
|
||
.map_err(|error| error.to_string())
|
||
}
|
||
|
||
/// 托管 JDK 可能存在的全部安装根(扫描运行时用)。
|
||
pub fn java_scan_roots(env: &HostEnv) -> Vec<PathBuf> {
|
||
java::location::scan_roots(
|
||
env.mode(),
|
||
&env.layout,
|
||
java_install_override(env).as_deref(),
|
||
env.platform,
|
||
&ProcessEnv,
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// 每个用例一个独立数据根(共享登记处是进程级的,键撞了会互相干扰)。
|
||
fn root(tag: &str) -> std::path::PathBuf {
|
||
static SEQ: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"ull-host-engine-{tag}-{}-{seq}",
|
||
std::process::id()
|
||
));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(dir.join(".ULL").join("cache")).expect("cache");
|
||
dir
|
||
}
|
||
|
||
#[test]
|
||
fn engine_uses_the_data_root_cache() {
|
||
let dir = root("cache");
|
||
let env = HostEnv::at_root(&dir);
|
||
let engine = engine(&env);
|
||
assert_eq!(engine.config().max_concurrency, 64);
|
||
// 地区判定不联网:只读缓存/信号
|
||
let _ = domestic(&env);
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
/// **M165 的守卫**:同一 host 的多次请求只建一个引擎。
|
||
///
|
||
/// 断言的是**构造次数**(`shared::constructions`),不是「能不能下到文件」——每请求现建一个
|
||
/// 引擎照样能下到文件,只有次数能把「复用」与「没复用」分开。
|
||
///
|
||
/// 抓得住的变异:把 `core::download::shared::engine_for` 里 `engines.get` 的命中分支删掉
|
||
/// (每次调用都现建),本用例红在 `constructions(...) == 1`。
|
||
#[test]
|
||
fn host_requests_share_one_engine() {
|
||
let dir = root("share");
|
||
let env = HostEnv::at_root(&dir);
|
||
let cache_root = env.layout.cache();
|
||
|
||
// 三条不同「请求」路径都从 engine() 取引擎(检索 / 清单 / 安装共用这一个漏斗)。
|
||
let first = engine(&env);
|
||
let second = engine(&env);
|
||
let third = engine(&HostEnv::at_root(&dir));
|
||
|
||
assert!(
|
||
Arc::ptr_eq(&first, &second) && Arc::ptr_eq(&second, &third),
|
||
"同一数据根的多次请求必须是同一个引擎"
|
||
);
|
||
assert_eq!(shared::constructions(&cache_root), 1, "只该构造一次引擎");
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
/// 不同数据根**不共享**引擎(缓存/健康度串味比多建一个引擎严重得多)。
|
||
#[test]
|
||
fn different_data_roots_get_their_own_engine() {
|
||
let dir_a = root("root-a");
|
||
let dir_b = root("root-b");
|
||
let env_a = HostEnv::at_root(&dir_a);
|
||
let env_b = HostEnv::at_root(&dir_b);
|
||
let engine_a = engine(&env_a);
|
||
let engine_b = engine(&env_b);
|
||
assert!(!Arc::ptr_eq(&engine_a, &engine_b));
|
||
assert_eq!(shared::constructions(env_a.layout.cache()), 1);
|
||
assert_eq!(shared::constructions(env_b.layout.cache()), 1);
|
||
std::fs::remove_dir_all(&dir_a).ok();
|
||
std::fs::remove_dir_all(&dir_b).ok();
|
||
}
|
||
|
||
/// `forget`(「重新检测地区」)之后下一次请求拿到新引擎。
|
||
#[test]
|
||
fn forget_makes_the_next_request_rebuild() {
|
||
let dir = root("forget");
|
||
let env = HostEnv::at_root(&dir);
|
||
let before = engine(&env);
|
||
forget(&env);
|
||
let after = engine(&env);
|
||
assert!(!Arc::ptr_eq(&before, &after));
|
||
assert_eq!(shared::constructions(env.layout.cache()), 2);
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
#[test]
|
||
fn java_merge_priority_reads_settings() {
|
||
let dir = root("priority");
|
||
let env = HostEnv::at_root(&dir);
|
||
std::fs::create_dir_all(env.layout.config()).expect("config");
|
||
std::fs::write(
|
||
env.launcher_config(),
|
||
r#"{"java":{"vendorPriority":["zulu"]}}"#,
|
||
)
|
||
.expect("写配置");
|
||
assert_eq!(java_priority(&env), vec![JdkVendor::Zulu]);
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
}
|