Files
Unlucky-Launcher/crates/presenter/src/host/engine.rs
T
AskaEth afe169751f fix(m165): forget 之后让自更新也换引擎(复审 P2-1)+ 卡口计数与端到端守卫
复审 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 干净。真站点仍未打(见报告「没验的部分」)。
2026-11-08 09:30:05 +08:00

198 lines
7.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 下载引擎与 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();
}
}