feat(presenter): 导入已有版本的 DTO 与三条后台线程(M223)
core 的能力(M221)接进 presenter 的 IPC 面: - dto/adopt.rs:AdoptCommand / AdoptEvent / 六种视图 / AdoptState + AdoptStage。 全部 Serialize+Deserialize,**不 import 任何 core 类型**(契约 §5.4); - host/adopt.rs:spawn_scan(adopt::scan)/ spawn_adopt(plan 或 adopt),结果经 HostTask::Adopt 回流;core 的候选/计划/结果/失败逐字段搬进 DTO,problems[] 一条不少; - CoreHost 加 adopt_epoch(每条命令 +1、关面板也 +1),连点与迟到的回程不会点亮面板; - 登记成功后补一次 refresh_instances(新实例这一拍就出现在列表里)。 运行目录只从 plan()/adopt() 的返回值取(缺省档可能带 -2 后缀,只有 core 知道用了哪个); target_notes() 的原文随 AdoptPlanView::notes 上屏,界面不另写一份后果话术。
This commit is contained in:
1 parent
9ab909d01c
commit
26e4fbb91d
6 files changed
+1012
No files matched your search
@@ -0,0 +1,261 @@
|
||||
//! 「导入已有版本」的宿主侧(M223 GUIADOPT):扫描 / 预检 / 确认三条后台线程。
|
||||
//!
|
||||
//! 能力全在 core(`unlucky_core::game::adopt`,M221):本模块只做三件事——**起线程、翻 DTO、
|
||||
//! 按代次回流**。这里不重新判定任何东西:能不能登记、落到哪个运行目录、有哪些问题,全部由 core
|
||||
//! 说了算,本模块逐字段照搬([`scan_view`] / [`plan_view`] / [`done_view`] / [`failure_view`])。
|
||||
//!
|
||||
//! ## 三个命令与写盘面
|
||||
//!
|
||||
//! | 命令 | core 调用 | 写盘 |
|
||||
//! |---|---|---|
|
||||
//! | [`AdoptCommand::Scan`] | `adopt::scan` | 零 |
|
||||
//! | [`AdoptCommand::Preview`] | `adopt::plan` | 零(预检就是 core 的 dry-run) |
|
||||
//! | [`AdoptCommand::Confirm`] | `adopt::adopt` | 实例设置 + `profiles.json`(+ 缺省运行目录) |
|
||||
//!
|
||||
//! 预检与确认走**同一个** `AdoptRequestView`:页面在预检拿到落点与后果原文,用户点确认时把
|
||||
//! **同一份请求**发回来(不是「预检 A、写 B」)。这也是「运行目录只认回程」的实现方式——
|
||||
//! 页面从头到尾没有机会自己派生一个目录名。
|
||||
//!
|
||||
//! ## 代次:为什么要它
|
||||
//!
|
||||
//! 三条命令各起一条线程,用户可能连点(扫完再扫、预检后改目标再预检、失败后关面板)。
|
||||
//! 每条线程带一个代次回来,`adopt_epoch` 只认**最新**那一条:关面板([`AdoptCommand::Dismiss`])
|
||||
//! 也会把代次 +1,于是「点了关闭,两秒后扫描结果又把面板点亮」这种旧病不会发生
|
||||
//! (它与 M105 安装取消、M113 导入取消是同一套做法)。
|
||||
//!
|
||||
//! ## 不做什么
|
||||
//!
|
||||
//! - **不自己派生运行目录**:`run_dir` 只从 `plan`/`adopt` 的返回里取(缺省档可能带 `-2` 后缀);
|
||||
//! - **不自己拼后果话术**:`notes` 原样来自 core 的 `target_notes()`,与 CLI 打的是同一句;
|
||||
//! - **不折叠问题**:`problems[]` 逐条搬进 [`AdoptFailureView`],一条都不省。
|
||||
|
||||
use unlucky_core::game::adopt::{
|
||||
self, AdoptCandidate, AdoptError, AdoptOutcome, AdoptPlan, AdoptProblem, AdoptRequest,
|
||||
AdoptTarget,
|
||||
};
|
||||
use unlucky_core::game::install::instance::Isolation;
|
||||
use unlucky_core::launch::time::{format_rfc3339_millis, now};
|
||||
|
||||
use crate::dto::{
|
||||
AdoptCandidateView, AdoptCommand, AdoptDoneView, AdoptEvent, AdoptFailureView,
|
||||
AdoptIsolationView, AdoptPlanView, AdoptProblemView, AdoptRequestView, AdoptScanView,
|
||||
AdoptTargetView, CoreEvent,
|
||||
};
|
||||
|
||||
use super::env::HostEnv;
|
||||
use super::job::{self, HostTask, JobSender};
|
||||
use super::CoreHost;
|
||||
|
||||
/// 后台那一步是「只看不写」还是「真的写」。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Mode {
|
||||
/// `adopt::plan`(预检,零写盘)
|
||||
Preview,
|
||||
/// `adopt::adopt`(真写)
|
||||
Confirm,
|
||||
}
|
||||
|
||||
impl CoreHost {
|
||||
/// 作废在飞的 adopt 结果,返回新代次(每条命令都调它,见模块文档「代次」一节)。
|
||||
fn next_adopt_epoch(&mut self) -> u64 {
|
||||
self.adopt_epoch = self.adopt_epoch.wrapping_add(1);
|
||||
self.adopt_epoch
|
||||
}
|
||||
|
||||
/// `AppCommand::Adopt` 的落点(M223)。
|
||||
///
|
||||
/// 每条命令都先把代次 +1:这一条之后回来的旧结果一律被 [`Self::handle_adopt`] 丢掉。
|
||||
pub(super) fn adopt(&mut self, command: AdoptCommand) {
|
||||
match command {
|
||||
AdoptCommand::Scan => {
|
||||
let epoch = self.next_adopt_epoch();
|
||||
spawn_scan(self.env.clone(), self.tx.clone(), epoch);
|
||||
}
|
||||
AdoptCommand::Preview(request) => {
|
||||
let epoch = self.next_adopt_epoch();
|
||||
spawn_adopt(
|
||||
self.env.clone(),
|
||||
self.tx.clone(),
|
||||
*request,
|
||||
Mode::Preview,
|
||||
epoch,
|
||||
);
|
||||
}
|
||||
AdoptCommand::Confirm(request) => {
|
||||
let epoch = self.next_adopt_epoch();
|
||||
spawn_adopt(
|
||||
self.env.clone(),
|
||||
self.tx.clone(),
|
||||
*request,
|
||||
Mode::Confirm,
|
||||
epoch,
|
||||
);
|
||||
}
|
||||
AdoptCommand::Dismiss => {
|
||||
// 关面板 = 作废在飞的一切(否则迟到的扫描结果会把面板重新点亮)
|
||||
self.next_adopt_epoch();
|
||||
self.send(CoreEvent::Adopt(Box::new(AdoptEvent::Cleared)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台线程的回流落点:代次不符就丢(见模块文档「代次」一节)。
|
||||
///
|
||||
/// 登记成功之后补一次实例快照刷新:新实例必须**这一拍就出现在实例列表里**,
|
||||
/// 而不是等用户手点刷新(否则「导入了但看不见」)。
|
||||
pub(super) fn handle_adopt(&mut self, event: AdoptEvent, epoch: u64) {
|
||||
if epoch != self.adopt_epoch {
|
||||
return;
|
||||
}
|
||||
let registered = matches!(event, AdoptEvent::Done(_));
|
||||
self.send(CoreEvent::Adopt(Box::new(event)));
|
||||
if registered {
|
||||
self.refresh_instances();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 扫描当前游戏根的 `versions/`(只读)。
|
||||
pub(super) fn spawn_scan(env: HostEnv, tx: JobSender, epoch: u64) {
|
||||
job::spawn("adopt-scan", move || {
|
||||
let event = match adopt::scan(&env.game, &env.layout) {
|
||||
Ok(scan) => AdoptEvent::Scan(Box::new(AdoptScanView {
|
||||
versions_dir: scan.versions_dir.clone(),
|
||||
candidates: scan.candidates.iter().map(candidate_view).collect(),
|
||||
})),
|
||||
// 读目录失败也是**具名**失败(断链的 versions 联接 / 权限),不降级成空列表
|
||||
Err(error) => AdoptEvent::Failed(Box::new(failure_view(error))),
|
||||
};
|
||||
let _ = tx.send(HostTask::Adopt {
|
||||
event: Box::new(event),
|
||||
epoch,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 预检(零写盘)或确认登记。
|
||||
pub(super) fn spawn_adopt(
|
||||
env: HostEnv,
|
||||
tx: JobSender,
|
||||
request: AdoptRequestView,
|
||||
mode: Mode,
|
||||
epoch: u64,
|
||||
) {
|
||||
job::spawn("adopt", move || {
|
||||
let request = core_request(request);
|
||||
let event = match mode {
|
||||
Mode::Preview => match adopt::plan(&env.game, &env.layout, &request) {
|
||||
Ok(plan) => AdoptEvent::Planned(Box::new(plan_view(plan))),
|
||||
Err(error) => AdoptEvent::Failed(Box::new(failure_view(error))),
|
||||
},
|
||||
Mode::Confirm => match adopt::adopt(&env.game, &env.layout, &request) {
|
||||
Ok(outcome) => AdoptEvent::Done(Box::new(done_view(outcome))),
|
||||
Err(error) => AdoptEvent::Failed(Box::new(failure_view(error))),
|
||||
},
|
||||
};
|
||||
let _ = tx.send(HostTask::Adopt {
|
||||
event: Box::new(event),
|
||||
epoch,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 视图请求 → core 请求(`created_at` 由宿主给:core 不碰时钟)。
|
||||
fn core_request(view: AdoptRequestView) -> AdoptRequest {
|
||||
AdoptRequest {
|
||||
version_id: view.version_id,
|
||||
name: view.name.filter(|name| !name.trim().is_empty()),
|
||||
target: target_of(view.target),
|
||||
created_at: Some(format_rfc3339_millis(now())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 三档视图 → core 的运行目录选择。
|
||||
///
|
||||
/// `custom` **不映射**:GUI 不开「用户手输绝对路径」这一档(那要目录选择器),
|
||||
/// 需要它的调用方走 CLI 的 `ull instances adopt --isolation custom --game-dir DIR`。
|
||||
fn target_of(view: AdoptTargetView) -> AdoptTarget {
|
||||
match view {
|
||||
AdoptTargetView::InstanceDir => AdoptTarget::InstanceDir,
|
||||
AdoptTargetView::Shared => AdoptTarget::Shared,
|
||||
AdoptTargetView::VersionDir => AdoptTarget::VersionDir,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 隔离档 → 视图。
|
||||
fn isolation_view(isolation: Isolation) -> AdoptIsolationView {
|
||||
match isolation {
|
||||
Isolation::Isolated => AdoptIsolationView::Isolated,
|
||||
Isolation::Shared => AdoptIsolationView::Shared,
|
||||
Isolation::Custom => AdoptIsolationView::Custom,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 候选 → 视图(`registered` 只带实例名:界面要的是「给谁登记过了」)。
|
||||
fn candidate_view(candidate: &AdoptCandidate) -> AdoptCandidateView {
|
||||
AdoptCandidateView {
|
||||
version_id: candidate.version_id.clone(),
|
||||
jar_present: candidate.jar_present,
|
||||
inherits: candidate.inherits.clone(),
|
||||
registered: candidate
|
||||
.registered
|
||||
.iter()
|
||||
.map(|instance| instance.name.clone())
|
||||
.collect(),
|
||||
problems: candidate.problems.iter().map(problem_view).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// core 问题 → 视图(`code` 稳定 + `Display` 原文:路径、行列号、缺哪个父版本都在里面)。
|
||||
fn problem_view(problem: &AdoptProblem) -> AdoptProblemView {
|
||||
AdoptProblemView {
|
||||
code: problem.code().to_string(),
|
||||
message: problem.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// core 计划 → 视图。`run_dir` / `notes` **原样**搬运,界面不许再算一遍。
|
||||
fn plan_view(plan: AdoptPlan) -> AdoptPlanView {
|
||||
AdoptPlanView {
|
||||
version_id: plan.candidate.version_id.clone(),
|
||||
name: plan.name,
|
||||
target: target_view(&plan.target),
|
||||
isolation: isolation_view(plan.isolation),
|
||||
run_dir: plan.run_dir,
|
||||
notes: plan.notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 结果 → 视图。
|
||||
fn done_view(outcome: AdoptOutcome) -> AdoptDoneView {
|
||||
AdoptDoneView {
|
||||
instance_id: outcome.instance_id,
|
||||
name: outcome.name,
|
||||
version_id: outcome.version_id,
|
||||
isolation: isolation_view(outcome.isolation),
|
||||
run_dir: outcome.run_dir,
|
||||
dir_created: outcome.dir_created,
|
||||
notes: outcome.notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 运行目录选择 → 视图(`Custom` 只在 core 侧出现,界面按「缺省档」显示)。
|
||||
fn target_view(target: &AdoptTarget) -> AdoptTargetView {
|
||||
match target {
|
||||
AdoptTarget::InstanceDir | AdoptTarget::Custom(_) => AdoptTargetView::InstanceDir,
|
||||
AdoptTarget::Shared => AdoptTargetView::Shared,
|
||||
AdoptTarget::VersionDir => AdoptTargetView::VersionDir,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 失败 → 视图:**`problems[]` 一条不少地带出来**(契约 §5.3)。
|
||||
fn failure_view(error: AdoptError) -> AdoptFailureView {
|
||||
AdoptFailureView {
|
||||
code: error.code().to_string(),
|
||||
message: error.to_string(),
|
||||
problems: error.problems().iter().map(problem_view).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,217 @@
|
||||
//! `host::adopt` 的用例(M223):真盘夹具 + 三条命令的回程形状。
|
||||
//!
|
||||
//! 抓得住:扫描只读(`.ULL/config` 不出现)、预检与确认的落点一致且都来自 core、
|
||||
//! 坏清单失败时 `problems[]` 逐条带出(带行列号)、三档映射。
|
||||
//! 抓不住:真启动游戏用不用这个运行目录(那是 launch 链路的活);D30 视觉(另走截图通道)。
|
||||
//!
|
||||
//! 为什么单独一个文件:`host/adopt.rs` 带用例会到 440 行,越过 400 行硬线(AGENTS §3.1)。
|
||||
|
||||
use super::*;
|
||||
use crate::dto::AdoptStage;
|
||||
|
||||
/// 真盘夹具:`<root>/.minecraft/versions/<id>/`(清单 + jar)+ `<root>/.ULL`。
|
||||
struct Fixture {
|
||||
root: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new(tag: &str) -> Self {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"ull-presenter-adopt-{tag}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_nanos())
|
||||
));
|
||||
std::fs::create_dir_all(&root).expect("建临时根");
|
||||
Self { root }
|
||||
}
|
||||
|
||||
fn env(&self) -> HostEnv {
|
||||
HostEnv::at_root(&self.root)
|
||||
}
|
||||
|
||||
/// 铺一个「别的启动器装好的」版本(`manifest` 原样写,便于造坏清单)。
|
||||
fn version(&self, id: &str, manifest: &str) {
|
||||
let dir = self.root.join(".minecraft/versions").join(id);
|
||||
std::fs::create_dir_all(&dir).expect("建版本目录");
|
||||
std::fs::write(dir.join(format!("{id}.json")), manifest).expect("写清单");
|
||||
std::fs::write(dir.join(format!("{id}.jar")), b"jar-bytes").expect("写 jar");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.root);
|
||||
}
|
||||
}
|
||||
|
||||
const MANIFEST: &str = r#"{
|
||||
"id": "1.20.1",
|
||||
"type": "release",
|
||||
"mainClass": "net.minecraft.main.Main",
|
||||
"libraries": []
|
||||
}"#;
|
||||
|
||||
fn request(version_id: &str, target: AdoptTargetView) -> AdoptRequestView {
|
||||
AdoptRequestView {
|
||||
version_id: version_id.to_string(),
|
||||
name: None,
|
||||
target,
|
||||
}
|
||||
}
|
||||
|
||||
/// 收一条 `HostTask::Adopt`(同步等后台线程交卷)。
|
||||
fn recv(rx: &super::super::job::JobReceiver) -> AdoptEvent {
|
||||
match rx.recv().expect("后台线程应回一条") {
|
||||
HostTask::Adopt { event, .. } => *event,
|
||||
other => panic!("期望 HostTask::Adopt,收到 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_lists_the_foreign_versions_without_writing() {
|
||||
let fixture = Fixture::new("scan");
|
||||
fixture.version("1.20.1", MANIFEST);
|
||||
let env = fixture.env();
|
||||
let (tx, rx) = job::channel();
|
||||
spawn_scan(env.clone(), tx, 1);
|
||||
|
||||
let AdoptEvent::Scan(scan) = recv(&rx) else {
|
||||
panic!("扫描应回 AdoptEvent::Scan");
|
||||
};
|
||||
assert_eq!(scan.versions_dir, fixture.root.join(".minecraft/versions"));
|
||||
assert_eq!(scan.total(), 1);
|
||||
assert_eq!(scan.adoptable(), 1);
|
||||
assert!(scan.candidates[0].jar_present);
|
||||
// 扫描零写盘:`.ULL/config` 下不该出现任何东西
|
||||
assert!(!fixture.root.join(".ULL/config/profiles.json").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_registers_and_reports_the_core_run_dir() {
|
||||
let fixture = Fixture::new("confirm");
|
||||
fixture.version("1.20.1", MANIFEST);
|
||||
let env = fixture.env();
|
||||
|
||||
// 先预检:零写盘,落点与后果原文都来自 core
|
||||
let (tx, rx) = job::channel();
|
||||
spawn_adopt(
|
||||
env.clone(),
|
||||
tx,
|
||||
request("1.20.1", AdoptTargetView::InstanceDir),
|
||||
Mode::Preview,
|
||||
1,
|
||||
);
|
||||
let AdoptEvent::Planned(plan) = recv(&rx) else {
|
||||
panic!("预检应回 AdoptEvent::Planned");
|
||||
};
|
||||
assert_eq!(plan.target, AdoptTargetView::InstanceDir);
|
||||
assert_eq!(plan.isolation, AdoptIsolationView::Custom);
|
||||
// 缺省档的运行目录落数据区(**不是** `versions/<id>`)
|
||||
assert!(
|
||||
plan.run_dir
|
||||
.starts_with(fixture.root.join(".ULL/data/instances")),
|
||||
"落点应在 ULL 数据区:{}",
|
||||
plan.run_dir.display()
|
||||
);
|
||||
assert!(!plan.notes.is_empty(), "缺省档也要说清后果");
|
||||
assert!(!fixture.root.join(".ULL/config/profiles.json").exists());
|
||||
|
||||
// 确认:同一份请求发回去,真的登记
|
||||
let (tx, rx) = job::channel();
|
||||
spawn_adopt(
|
||||
env,
|
||||
tx,
|
||||
request("1.20.1", AdoptTargetView::InstanceDir),
|
||||
Mode::Confirm,
|
||||
2,
|
||||
);
|
||||
let AdoptEvent::Done(done) = recv(&rx) else {
|
||||
panic!("确认应回 AdoptEvent::Done");
|
||||
};
|
||||
assert_eq!(done.version_id, "1.20.1");
|
||||
assert_eq!(done.run_dir, plan.run_dir, "确认的落点必须与预检一致");
|
||||
assert!(done.dir_created);
|
||||
let profiles = std::fs::read_to_string(fixture.root.join(".ULL/config/profiles.json"))
|
||||
.expect("profiles.json 应被写出来");
|
||||
assert!(profiles.contains("1.20.1"));
|
||||
// 游戏根的版本目录一个字节都没写(只有夹具自己那两个文件)
|
||||
let dir = fixture.root.join(".minecraft/versions/1.20.1");
|
||||
let mut names: Vec<String> = std::fs::read_dir(&dir)
|
||||
.expect("读版本目录")
|
||||
.map(|entry| {
|
||||
entry
|
||||
.expect("条目")
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect();
|
||||
names.sort();
|
||||
assert_eq!(names, ["1.20.1.jar", "1.20.1.json"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bad_manifest_comes_back_as_every_problem_not_one_sentence() {
|
||||
let fixture = Fixture::new("problems");
|
||||
fixture.version("1.20.1", "{ this is not json");
|
||||
let (tx, rx) = job::channel();
|
||||
spawn_adopt(
|
||||
fixture.env(),
|
||||
tx,
|
||||
request("1.20.1", AdoptTargetView::InstanceDir),
|
||||
Mode::Preview,
|
||||
1,
|
||||
);
|
||||
let AdoptEvent::Failed(failure) = recv(&rx) else {
|
||||
panic!("坏清单应回 AdoptEvent::Failed");
|
||||
};
|
||||
assert_eq!(failure.code, "adopt_not_adoptable");
|
||||
assert_eq!(failure.problems.len(), 1);
|
||||
assert_eq!(failure.problems[0].code, "manifest_invalid_json");
|
||||
assert!(
|
||||
failure.problems[0].message.contains("line"),
|
||||
"清单问题要带行列号:{}",
|
||||
failure.problems[0].message
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn targets_map_to_core_and_custom_stays_out_of_the_gui() {
|
||||
assert_eq!(
|
||||
target_of(AdoptTargetView::InstanceDir),
|
||||
AdoptTarget::InstanceDir
|
||||
);
|
||||
assert_eq!(target_of(AdoptTargetView::Shared), AdoptTarget::Shared);
|
||||
assert_eq!(
|
||||
target_of(AdoptTargetView::VersionDir),
|
||||
AdoptTarget::VersionDir
|
||||
);
|
||||
// `Custom` 只在 core 侧出现(CLI 的 `--game-dir`):界面按缺省档显示
|
||||
assert_eq!(
|
||||
target_view(&AdoptTarget::Custom(std::path::PathBuf::from("/x"))),
|
||||
AdoptTargetView::InstanceDir
|
||||
);
|
||||
assert_eq!(
|
||||
isolation_view(Isolation::Custom),
|
||||
AdoptIsolationView::Custom
|
||||
);
|
||||
assert_eq!(
|
||||
isolation_view(Isolation::Shared),
|
||||
AdoptIsolationView::Shared
|
||||
);
|
||||
assert_eq!(
|
||||
isolation_view(Isolation::Isolated),
|
||||
AdoptIsolationView::Isolated
|
||||
);
|
||||
// 空名回落到版本 id 由 core 负责;这里只钉「空白名不进请求」
|
||||
let request = core_request(AdoptRequestView {
|
||||
version_id: "1.20.1".to_string(),
|
||||
name: Some(" ".to_string()),
|
||||
target: AdoptTargetView::Shared,
|
||||
});
|
||||
assert_eq!(request.name, None);
|
||||
assert!(request.created_at.is_some());
|
||||
assert_eq!(AdoptStage::Pick.slug(), "pick");
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
mod accounts;
|
||||
mod addon;
|
||||
// M223:导入已有版本(扫描 / 预检 / 确认三条后台线程)
|
||||
mod adopt;
|
||||
mod dispatch;
|
||||
mod engine;
|
||||
pub mod env;
|
||||
@@ -106,6 +108,10 @@ pub struct CoreHost {
|
||||
/// 一眼能看出「这是旧任务的」,于是既不会被取消掉的安装重新点亮面板,也不会串到下一轮安装
|
||||
/// (两轮安装同时存在时,旧任务的事件曾经把新任务的进度条顶掉)。
|
||||
install_epoch: u64,
|
||||
/// 「导入已有版本」的代次(M223):每条 scan/preview/confirm 命令 +1,关面板也 +1。
|
||||
///
|
||||
/// 只认最新代次的回程事件——用户连点或点了关闭之后,旧线程带回来的结果不许再把面板点亮。
|
||||
adopt_epoch: u64,
|
||||
/// 自更新(M159):检查结论 / 应用状态 / 更新域偏好。
|
||||
update: update_state::Runtime,
|
||||
}
|
||||
@@ -160,6 +166,7 @@ impl CoreHost {
|
||||
chat: MockBackend::default(),
|
||||
install_cancel: None,
|
||||
install_epoch: 0,
|
||||
adopt_epoch: 0,
|
||||
update,
|
||||
};
|
||||
if refresh {
|
||||
|
||||
Reference in new issue
Block a user