fix(m195): 复审 r1 三项 —— 解压检查点真守卫、presenter 接线测试、取消文案改回真话
P2-1(解压循环检查点没有真守卫):那条用例原来的断言(status/target/files/bytes/无残留暂存) 在删掉 `extract_overrides` 的检查点后**依然成立**——提交前检查点⑤会替它停一次,于是几 GB 的包 会一直解到结束而测试全绿。改成数 `ImportStage::Extract` 事件:有检查点 = 1 条、删掉 = 3 条 (两面实测)。模块文档同时如实登记"还没有真守卫"的检查点:②③④⑤、全新导入 commit 的取消 分支、phase.rs 远端下载逐条、complete.rs 补全循环的 break。 P2-2(presenter 的真令牌接线零测试):新增 `pack/tests/cancel.rs`,直接调 `import::run` 并把 传入的 `Arc<AtomicBool>` **预先置 true**,判据只能是盘上事实(目标目录不存在 + profiles.json 零实例)——"没收到 Report"那种判据两条路都成立,`run` 早退条件里的 `cancel.load()` 会把 "没接线"完全遮住。 P2-3(用户可见文案说反了话):`PackCancelled` 中英双语从「导入会在后台继续跑完 / Import keeps running in the background…已经写下的文件会留下」改为「导入已取消 —— 后台已停手并回滚:没有留下 半包,也没有覆盖已有文件」;golden 金表同步;`model/tests.rs` 那条把旧文案钉死的用例改成钉新 语义(必须说清停手+回滚,且**禁止**出现 background);`model.rs` 的 `cancelled` 字段文档与 `dispatch.rs` 的取消注释同族修正。 验证:`cargo test -p unlucky-core -p unlucky-presenter` 1480 passed / 0 failed; i18n 31 / 0;app-iced packui 28 / 0;三 crate clippy 零 warning。
This commit is contained in:
1 parent
1441a240fe
commit
db5b08b9ab
9 files changed
+133
-29
No files matched your search
@@ -196,8 +196,9 @@ pub struct Pack {
|
|||||||
pub running: Option<Running>,
|
pub running: Option<Running>,
|
||||||
/// 探测失败诊断
|
/// 探测失败诊断
|
||||||
pub probe: Option<Probe>,
|
pub probe: Option<Probe>,
|
||||||
/// 用户取消过这次导入:面板收回但**后台还在跑**(core 没有取消令牌),
|
/// 用户取消过这次导入:面板收回,**后台也真的停手并回滚了**(M195 起 core 有取消令牌:
|
||||||
/// 所以要把这句话留在界面上,别让用户以为「没建成」(复审 r1 P1-2)。
|
/// 暂存目录删掉、落盘过的撤回、实例不登记)。这句话仍要留在界面上——取消不是"什么都没
|
||||||
|
/// 发生",而是"按你的意思停下来了",别让用户以为点空了。
|
||||||
pub cancelled: bool,
|
pub cancelled: bool,
|
||||||
/// 取消提示的正文
|
/// 取消提示的正文
|
||||||
pub cancelled_note: &'static str,
|
pub cancelled_note: &'static str,
|
||||||
|
|||||||
@@ -382,19 +382,17 @@ fn overwrite_is_offered_only_when_importing_into_an_existing_instance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cancelled_state_is_visible_and_says_the_import_keeps_running() {
|
fn cancelled_state_is_visible_and_says_the_import_stopped() {
|
||||||
// 复审 r1 P1-2:面板收回之后,界面上必须留一句「后台还在跑」
|
// M195:取消是管线级的(core 停手并回滚),旧文案「后台还在跑」已是假话。
|
||||||
let import = unlucky_presenter::dto::ImportState {
|
let import = unlucky_presenter::dto::ImportState {
|
||||||
cancelled: true,
|
cancelled: true,
|
||||||
..unlucky_presenter::dto::ImportState::default()
|
..unlucky_presenter::dto::ImportState::default()
|
||||||
};
|
};
|
||||||
let export = unlucky_presenter::dto::ExportState::default();
|
let export = unlucky_presenter::dto::ExportState::default();
|
||||||
let model = Pack::of(&import, &export, &PackPageState::default(), &[], catalog());
|
let model = Pack::of(&import, &export, &PackPageState::default(), &[], catalog());
|
||||||
assert!(model.cancelled);
|
let note = model.cancelled_note.to_lowercase();
|
||||||
assert!(
|
let stopped = note.contains("stopped") && note.contains("rolled back");
|
||||||
model.cancelled_note.to_lowercase().contains("background"),
|
assert!(model.cancelled && !model.is_idle(), "取消态不能当成空闲");
|
||||||
"取消提示必须说清后台还在跑:{}",
|
assert!(stopped, "文案要说清停手并回滚");
|
||||||
model.cancelled_note
|
assert!(!note.contains("background"), "文案不许说后台跑");
|
||||||
);
|
|
||||||
assert!(!model.is_idle(), "取消态不能被当成「什么都没有」");
|
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,26 @@
|
|||||||
//! | 用例 | 删掉哪一处就红 |
|
//! | 用例 | 删掉哪一处就红 |
|
||||||
//! |------|----------------|
|
//! |------|----------------|
|
||||||
//! | `a_cancel_before_the_first_write_touches_nothing` | 入口检查点 |
|
//! | `a_cancel_before_the_first_write_touches_nothing` | 入口检查点 |
|
||||||
//! | `a_cancel_inside_the_extract_loop_leaves_no_half_unpacked_pack` | 解压循环里的检查点 |
|
//! | `a_cancel_inside_the_extract_loop_leaves_no_half_unpacked_pack` | 解压循环检查点(判据 = 解压进度条数:有 = 1、删掉 = 3) |
|
||||||
//! | `a_cancel_in_the_middle_of_the_merge_restores_the_users_file` | 取消时的 `rollback_merge` |
|
//! | `a_cancel_in_the_middle_of_the_merge_restores_the_users_file` | 取消时的 `rollback_merge` |
|
||||||
//! | `a_cancel_right_before_registering_rolls_the_whole_instance_back` | 登记前的检查点 |
|
//! | `a_cancel_right_before_registering_rolls_the_whole_instance_back` | 登记前的检查点 |
|
||||||
//! | `cancel_and_failure_are_two_different_result_types` | 把取消并进 `fail()` 那条路 |
|
//! | `cancel_and_failure_are_two_different_result_types` | 把取消并进 `fail()` 那条路 |
|
||||||
//!
|
//!
|
||||||
//! 取消的触发点全部走**进度回调**(回调里置位 = 用户此刻点了取消,没有 sleep、没有竞态)。
|
//! # 解压那条必须数事件,其余没有真守卫的检查点
|
||||||
//! 全部离线:手造 mrpack(`files` 为空,内容全在 `overrides/`)。
|
//!
|
||||||
|
//! 解压只写**暂存目录**,所以"status == Cancelled / 目标目录不存在 / `files` 为空 / 无残留暂存"
|
||||||
|
//! 在删掉循环检查点后**依然成立**(提交前检查点会替它停一次)——只有"解了几条"能区分两者。
|
||||||
|
//!
|
||||||
|
//! 有变异证明的只有上表五处。**没有**:检查点②解析后 / ③补全后 / ④开暂存目录前 / ⑤提交前(删掉
|
||||||
|
//! 任一处,别的检查点会接住同一次取消,`pipeline.rs` 的断言仍绿)、全新导入 `commit` 的取消分支、
|
||||||
|
//! [`super::phase`] 远端下载逐条检查点、[`super::complete`] 补全循环的 `break`——补它们要两段式
|
||||||
|
//! 夹具或更多计数探针,本轮没做。
|
||||||
|
//!
|
||||||
|
//! 触发点全部走**进度回调**(置位 = 用户此刻点了取消,无 sleep、无竞态);全部离线。
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::cancel::ImportCancel;
|
use super::cancel::ImportCancel;
|
||||||
@@ -35,7 +45,7 @@ const INDEX: &str = r#"{
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
/// 手造一个 mrpack(`entries` 是 `overrides/` 下的实例内路径与字节)。
|
/// 手造 mrpack(`entries` 是 `overrides/` 下的实例内路径与字节)。
|
||||||
fn mrpack(tag: &str, entries: &[(&str, &[u8])]) -> (TempRoot, PathBuf) {
|
fn mrpack(tag: &str, entries: &[(&str, &[u8])]) -> (TempRoot, PathBuf) {
|
||||||
let root = TempRoot::new(tag);
|
let root = TempRoot::new(tag);
|
||||||
let source = root.join("src");
|
let source = root.join("src");
|
||||||
@@ -105,11 +115,28 @@ fn cancel_on(token: &ImportCancel, stage: ImportStage) -> ImportProgressSink {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在**合并态第二条逐文件进度**上置位取消。
|
/// 数**解压阶段**的进度条数,并在第一条上置位取消(这是解压循环检查点唯一的可观测判据:
|
||||||
///
|
/// 有它 ⇒ 第一条之后立刻停 = 1;删掉 ⇒ 一路解完 = 文件条数)。
|
||||||
/// 合并是**排序后**逐文件搬:第 0 条是基线(`.ull/pack-baseline.json`,`.` 排在最前),第 1 条
|
fn cancel_on_first_extract_counting(
|
||||||
/// 才是 `mods/a.jar`。所以 `done == 1` 正好是"用户那个被覆盖的文件已经搬进去、后面还有"的
|
token: &ImportCancel,
|
||||||
/// 窗口——journal 回滚要接住的正是这一刻(`total > 0` 顺带把阶段开始那条 0/0 排除掉)。
|
) -> (ImportProgressSink, Arc<AtomicUsize>) {
|
||||||
|
let token = token.clone();
|
||||||
|
let count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let seen = Arc::clone(&count);
|
||||||
|
let sink: ImportProgressSink = Arc::new(move |progress: &ImportProgress| {
|
||||||
|
if progress.stage != ImportStage::Extract {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if seen.fetch_add(1, Ordering::SeqCst) == 0 {
|
||||||
|
token.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(sink, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在**合并态第二条逐文件进度**上置位取消。合并是排序后逐文件搬:第 0 条是基线
|
||||||
|
/// (`.ull/pack-baseline.json`,`.` 排最前),第 1 条才是 `mods/a.jar`——`done == 1` 正好是
|
||||||
|
/// "用户那个被覆盖的文件已经搬进去、后面还有"的窗口,journal 回滚要接住的正是这一刻。
|
||||||
fn cancel_on_second_merged_file(token: &ImportCancel) -> ImportProgressSink {
|
fn cancel_on_second_merged_file(token: &ImportCancel) -> ImportProgressSink {
|
||||||
let token = token.clone();
|
let token = token.clone();
|
||||||
Arc::new(move |progress: &ImportProgress| {
|
Arc::new(move |progress: &ImportProgress| {
|
||||||
@@ -119,7 +146,7 @@ fn cancel_on_second_merged_file(token: &ImportCancel) -> ImportProgressSink {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 目标目录里**全部文件**的实例内相对路径(排序后),用来钉"取消后与导入前逐文件相同"。
|
/// 目标目录里全部文件的实例内相对路径(排序后):钉"取消后与导入前逐文件相同"。
|
||||||
fn files_under(root: &Path) -> Vec<String> {
|
fn files_under(root: &Path) -> Vec<String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut pending = vec![root.to_path_buf()];
|
let mut pending = vec![root.to_path_buf()];
|
||||||
@@ -193,14 +220,22 @@ fn a_cancel_inside_the_extract_loop_leaves_no_half_unpacked_pack() {
|
|||||||
let (root, pack) = three_files("cancel-extract");
|
let (root, pack) = three_files("cancel-extract");
|
||||||
let target = root.join("instance");
|
let target = root.join("instance");
|
||||||
let token = ImportCancel::new();
|
let token = ImportCancel::new();
|
||||||
|
let (sink, extract_events) = cancel_on_first_extract_counting(&token);
|
||||||
|
|
||||||
let report = block_on(import_pack(
|
let report = block_on(import_pack(
|
||||||
&ImportRequest::new(&pack, &target)
|
&ImportRequest::new(&pack, &target)
|
||||||
.with_cancel(&token)
|
.with_cancel(&token)
|
||||||
.with_progress(cancel_on(&token, ImportStage::Extract)),
|
.with_progress(sink),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(report.status, ImportStatus::Cancelled, "{:?}", report.error);
|
assert_eq!(report.status, ImportStatus::Cancelled, "{:?}", report.error);
|
||||||
|
// **这一条才是解压循环检查点的判据**:有它 ⇒ 第一条之后立刻停(3 个文件的包里只报了 1 条
|
||||||
|
// 解压进度);删掉它 ⇒ 一路解完(3 条)。其余断言在删掉检查点后仍然成立,挡不住回归。
|
||||||
|
assert_eq!(
|
||||||
|
extract_events.load(Ordering::SeqCst),
|
||||||
|
1,
|
||||||
|
"取消必须在第一条解压之后立刻生效(包里有 3 个文件,解完再停 = 解压段没有检查点)"
|
||||||
|
);
|
||||||
assert!(report.error.is_none(), "取消不许带错误");
|
assert!(report.error.is_none(), "取消不许带错误");
|
||||||
assert!(!target.exists(), "不许留下半包:{}", target.display());
|
assert!(!target.exists(), "不许留下半包:{}", target.display());
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -179,8 +179,8 @@ macro_rules! content_entries {
|
|||||||
),
|
),
|
||||||
PackReportRejectedNote => ("not supported by the client", "客户端不支持"),
|
PackReportRejectedNote => ("not supported by the client", "客户端不支持"),
|
||||||
PackCancelled => (
|
PackCancelled => (
|
||||||
"Import keeps running in the background — this result is discarded. Files already written stay.",
|
"Import cancelled — it stopped and rolled back: nothing was left half-done, no existing files were overwritten.",
|
||||||
"导入会在后台继续跑完 —— 本次结果作废;已经写下的文件会留下。"
|
"导入已取消 —— 后台已停手并回滚:没有留下半包,也没有覆盖已有文件。"
|
||||||
),
|
),
|
||||||
PackOverwriteAction => ("Re-import, the pack wins", "以包为准重新导入"),
|
PackOverwriteAction => ("Re-import, the pack wins", "以包为准重新导入"),
|
||||||
PackOverwriteNote => (
|
PackOverwriteNote => (
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[
|
|||||||
("PackReportConflicts", "Conflicts: {count}", "冲突:{count} 个"),
|
("PackReportConflicts", "Conflicts: {count}", "冲突:{count} 个"),
|
||||||
("PackReportRejected", "The client does not support these: {count}", "这些组件客户端不支持:{count} 个"),
|
("PackReportRejected", "The client does not support these: {count}", "这些组件客户端不支持:{count} 个"),
|
||||||
("PackReportRejectedNote", "not supported by the client", "客户端不支持"),
|
("PackReportRejectedNote", "not supported by the client", "客户端不支持"),
|
||||||
("PackCancelled", "Import keeps running in the background — this result is discarded. Files already written stay.", "导入会在后台继续跑完 —— 本次结果作废;已经写下的文件会留下。"),
|
("PackCancelled", "Import cancelled — it stopped and rolled back: nothing was left half-done, no existing files were overwritten.", "导入已取消 —— 后台已停手并回滚:没有留下半包,也没有覆盖已有文件。"),
|
||||||
("PackOverwriteAction", "Re-import, the pack wins", "以包为准重新导入"),
|
("PackOverwriteAction", "Re-import, the pack wins", "以包为准重新导入"),
|
||||||
("PackOverwriteNote", "Keeping both (renamed) is not supported yet: for each existing file you pick either “keep” or “overwrite”.", "「两者都留(改名)」暂不支持:每个已存在的文件只能选「保留」或「覆盖」。"),
|
("PackOverwriteNote", "Keeping both (renamed) is not supported yet: for each existing file you pick either “keep” or “overwrite”.", "「两者都留(改名)」暂不支持:每个已存在的文件只能选「保留」或「覆盖」。"),
|
||||||
("PackMissingOffline", "offline import", "离线导入"),
|
("PackMissingOffline", "offline import", "离线导入"),
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ pub(in crate::host) fn in_flight(env: &HostEnv) -> usize {
|
|||||||
///
|
///
|
||||||
/// 复审 r2 P2-1 的教训:手工 `finished()` 只在 `block_on(import_pack(..))` 之后调用,
|
/// 复审 r2 P2-1 的教训:手工 `finished()` 只在 `block_on(import_pack(..))` 之后调用,
|
||||||
/// 而失败早退(探测失败 / 目标目录不可用)都在它之前 ⇒ 计数永远留在「在跑」,于是下一次
|
/// 而失败早退(探测失败 / 目标目录不可用)都在它之前 ⇒ 计数永远留在「在跑」,于是下一次
|
||||||
/// **下载页**的「取消安装」不再发 `InstallCancelled`,还给内容库画一句「导入还在后台跑」的假话。
|
/// **下载页**的「取消安装」不再发 `InstallCancelled`,还给内容库画一句「导入还在跑」的假话。
|
||||||
///
|
///
|
||||||
/// 现在计数由句柄的 `Drop` 负责:**任何**退出路径(早退、报错、`?`、panic 展开)都会减一。
|
/// 现在计数由句柄的 `Drop` 负责:**任何**退出路径(早退、报错、`?`、panic 展开)都会减一。
|
||||||
pub(super) struct InFlight {
|
pub(super) struct InFlight {
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
//! | [`a_failed_new_instance_directory_can_be_reused_for_retry`] | 失败残留的空目录不挡重试 |
|
//! | [`a_failed_new_instance_directory_can_be_reused_for_retry`] | 失败残留的空目录不挡重试 |
|
||||||
//! | [`pack_epoch_is_per_host_and_is_its_own_counter`] | 代次按 host 分开,不再跨 host 串扰 |
|
//! | [`pack_epoch_is_per_host_and_is_its_own_counter`] | 代次按 host 分开,不再跨 host 串扰 |
|
||||||
//! | [`a_failed_import_releases_the_in_flight_flag_and_keeps_install_cancel_intact`] | 早退路径放回「在跑」计数,取消安装仍是安装的取消 |
|
//! | [`a_failed_import_releases_the_in_flight_flag_and_keeps_install_cancel_intact`] | 早退路径放回「在跑」计数,取消安装仍是安装的取消 |
|
||||||
|
//! | [`cancel::a_pre_cancelled_flag_really_stops_the_core_pipeline`] | 取消标记**真的**交给 core(盘上零残留、档案零实例) |
|
||||||
//!
|
//!
|
||||||
//! 夹具(临时目录、写文件、泵 host)在 [`harness`]。
|
//! 夹具(临时目录、写文件、泵 host)在 [`harness`]。
|
||||||
|
|
||||||
|
mod cancel;
|
||||||
mod harness;
|
mod harness;
|
||||||
mod scan;
|
mod scan;
|
||||||
|
|
||||||
@@ -388,7 +390,7 @@ fn a_failed_import_releases_the_in_flight_flag_and_keeps_install_cancel_intact()
|
|||||||
!pack_events(&events)
|
!pack_events(&events)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| matches!(event, PackEvent::Cancelled)),
|
.any(|event| matches!(event, PackEvent::Cancelled)),
|
||||||
"此刻没有导入在跑,不该给内容库发「导入还在后台跑」"
|
"此刻没有导入在跑,不该给内容库发「导入已取消」"
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! presenter 的**真令牌**接线(M195 复审 P2-2):取消标记必须真的交给 core。
|
||||||
|
//!
|
||||||
|
//! # 为什么判据只能是盘上的事实
|
||||||
|
//!
|
||||||
|
//! 只断言"没收到 `PackEvent::Report`"是**两条路都成立**的伪证明:`import::run` 的早退条件是
|
||||||
|
//! `report.status == Cancelled || cancel.load()`,后者会把"根本没接线"完全遮住——core 照旧跑完
|
||||||
|
//! 并登记实例,界面只看到面板收回。那正是本 mission 要消灭的形态,所以本用例盯的是**目标目录
|
||||||
|
//! 与 `profiles.json`**:标记一进来就是 true ⇒ 目录里一个文件都不该有、实例一个都不该登记。
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::dto::{ConflictDecisionsView, ImportRequestView};
|
||||||
|
|
||||||
|
use super::harness::{temp_dir, write};
|
||||||
|
|
||||||
|
/// 一个能走完管线的最小 MultiMC 包(目录形态:不需要 zip 写入器)。
|
||||||
|
fn multimc_pack(dir: &std::path::Path) -> std::path::PathBuf {
|
||||||
|
let source = dir.join("Better MC");
|
||||||
|
write(
|
||||||
|
&source.join("mmc-pack.json"),
|
||||||
|
br#"{"formatVersion": 1, "components": [
|
||||||
|
{"uid": "net.minecraft", "version": "1.20.1"}]}"#,
|
||||||
|
);
|
||||||
|
write(&source.join(".minecraft/mods/sodium.jar"), &[0x11; 1024]);
|
||||||
|
write(&source.join(".minecraft/config/options.txt"), b"fov=90\n");
|
||||||
|
source
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_pre_cancelled_flag_really_stops_the_core_pipeline() {
|
||||||
|
let dir = temp_dir("cancel-wired");
|
||||||
|
let source = multimc_pack(&dir);
|
||||||
|
let data = dir.join("data");
|
||||||
|
std::fs::create_dir_all(&data).expect("data root");
|
||||||
|
let env = crate::host::HostEnv::at_root(&data);
|
||||||
|
let request = ImportRequestView {
|
||||||
|
pack: source,
|
||||||
|
instance: None,
|
||||||
|
instance_name: Some("Cancelled Pack".to_string()),
|
||||||
|
decisions: ConflictDecisionsView::default(),
|
||||||
|
};
|
||||||
|
// 目标目录与"是否登记"都按 host 的真口径算(判据不能自己另算一份)。
|
||||||
|
let (target, register) = super::super::import::target_of(&env, &request).expect("目标目录");
|
||||||
|
assert!(register, "走新实例这条路才覆盖「登记」这一环");
|
||||||
|
|
||||||
|
let (tx, _rx) = crate::host::job::channel();
|
||||||
|
let cancel = Arc::new(AtomicBool::new(true));
|
||||||
|
super::super::import::run(&env, &tx, &request, 11, Arc::clone(&cancel));
|
||||||
|
drop(tx);
|
||||||
|
assert!(cancel.load(Ordering::SeqCst), "用例前提:标记一直是 true");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!target.exists(),
|
||||||
|
"取消后目标目录不该存在(core 在入口检查点就停手,暂存目录也删了):{}",
|
||||||
|
target.display()
|
||||||
|
);
|
||||||
|
let profiles =
|
||||||
|
unlucky_core::game::install::instance::read_profiles(&env.layout).expect("读档案");
|
||||||
|
assert!(
|
||||||
|
profiles.profiles.is_empty(),
|
||||||
|
"取消后不许登记实例:{:?}",
|
||||||
|
profiles.profiles
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
@@ -72,9 +72,10 @@ impl CoreHost {
|
|||||||
if let Some(cancel) = &self.install_cancel {
|
if let Some(cancel) = &self.install_cancel {
|
||||||
cancel.store(true, std::sync::atomic::Ordering::SeqCst);
|
cancel.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
// M113: import/export —— 同一块面板的「取消」对导入也生效:立标记 + 作废 pack 代次
|
// M113: import/export —— 同一块面板的「取消」对导入也生效:立标记 + 作废 pack 代次。
|
||||||
// 返回值告诉我们「刚才有没有导入在跑」:有的话**不能**再发 install 那条取消信号
|
// 返回值告诉我们「刚才有没有导入在跑」:有的话**不能**再发 install 那条取消信号
|
||||||
// (它说的是「安装已取消、实例未创建」,而导入会在后台跑完并登记实例)。
|
// (它说的是「安装已取消、实例未创建」,而导入的取消是**管线级**的:core 在检查点
|
||||||
|
// 停手并回滚,文案见 `PackCancelled`;M195 之前它会在后台跑完并登记实例)。
|
||||||
let pack_was_running = addon::cancel_pack(&self.env);
|
let pack_was_running = addon::cancel_pack(&self.env);
|
||||||
if pack_was_running {
|
if pack_was_running {
|
||||||
self.send(CoreEvent::Pack(Box::new(PackEvent::Cancelled)));
|
self.send(CoreEvent::Pack(Box::new(PackEvent::Cancelled)));
|
||||||
|
|||||||
Reference in new issue
Block a user