feat(m195): 导入管线取消令牌与原子回滚(取消不是失败)
M190 复核 §3 A-2:导入管线没有取消令牌,「取消导入」只改界面状态,后台仍跑完并写盘
(半包 / 覆盖风险,丢数据档)。本轮把取消做成管线级:
- core 新增 `import/cancel.rs`:`ImportCancel` 令牌(可 `from_flag` 接管既有
`Arc<AtomicBool>`)+ 内部 `Stopped::{Cancelled, Failed}` 两条停止路线;
- 结果类型单列 `ImportStatus::Cancelled`(`error` 恒为 None、退出码 130、文案 key 独立),
与 `Failed` 是两条路——UI 不弹红、脚本不会把「改主意」记成「装坏了」;
- 七个检查点:入口 → 解析后 → 补全后 → 开暂存目录前 → 提交前 → 合并态逐文件 → 登记前;
解压 / 远端下载各逐条检查;单条下载在飞不可中断(fetcher 无取消参数)已在文档里说准;
- 取消后不留半包 / 不覆盖已有实例:暂存目录整个删、合并态 journal 逆序回滚(备份改回原名)、
落盘后取消连内容一起回滚;`files / conflicts / missing / bytes_written` 清零,不留假账;
- presenter:同一个取消标记现在真交给 core(`with_cancel`),取消后不再「后台跑完并登记」;
CLI 增 `cancelled → 130` 一档与三档人读名。
守门三条各带变异证明(见 mission 报告):删回滚 → 红、删登记前检查 → 红、把取消并进
`fail()` → 红。
没验:超大整合包的真实 IO 下取消响应延迟(最坏 = 当前一条文件读完/下完 + 一次回滚)、
单条下载在飞不可中断、macOS / Linux 未跑。
This commit is contained in:
1 parent
3116f43c9f
commit
1441a240fe
18 files changed
+1144
-189
No files matched your search
@@ -24,19 +24,20 @@
|
||||
//! 所以每个可扫文件都证明不了可被远程替代 ⇒ 内联;`SkippedLocal` 就是「无法导出」。
|
||||
//! `tests::preview_counts_match_the_written_report` 用真管线钉住这个等式。
|
||||
//!
|
||||
//! ## 取消是**界面级**的
|
||||
//! ## 取消是**管线级**的(M195 起)
|
||||
//!
|
||||
//! [`cancel_pack`] 置模块级标记 + `dispatch` 里的 `CancelInstall` 把代次 +1:
|
||||
//! [`cancel_pack`] 置模块级标记 + `dispatch` 里的 `CancelInstall` 把代次 +1,而**同一个标记**
|
||||
//! 从 M195 起就是 core 的取消令牌(`ImportRequest::with_cancel`):
|
||||
//!
|
||||
//! 1. 面板立刻收回(不是等后台回一句话);
|
||||
//! 2. 这一轮之后回来的事件因代次不符被 `handle` 丢掉;
|
||||
//! 3. **后台那次导入会继续跑到结束**——core 的 `ImportRequest` 没有取消令牌,进度回调也拿不到
|
||||
//! 控制流。所以「取消」的真实含义是「不再显示、结果作废」,不是「把它停手」。要真停必须给
|
||||
//! core 加 cancel token(已登记 finding,报告里同样划出来);
|
||||
//! 4. **提示语不能借用安装那条**(复审 r1 P1-2):下载页的取消 toast 写着「实例未创建」,
|
||||
//! 而导入会把实例**登记上**。所以 `cancel_pack()` 回一个「刚才有没有导入在跑」,
|
||||
//! `dispatch` 据此改发 [`crate::dto::PackEvent::Cancelled`],界面上换成
|
||||
//! 「导入会在后台继续跑完 —— 本次结果作废」这句。
|
||||
//! 3. **后台那次导入真的停手**:core 在阶段边界与长循环内检查令牌,停下就回滚(暂存目录删掉、
|
||||
//! 落盘过的撤回、实例不登记),结果是 `ImportStatus::Cancelled` 而**不是**失败。响应延迟的
|
||||
//! 底线是「当前这一条文件读完/下完」,见 `core::addon::import::cancel` 的逐段表;
|
||||
//! 4. **提示语不能借用安装那条**(复审 r1 P1-2):`cancel_pack()` 回一个「刚才有没有导入在跑」,
|
||||
//! `dispatch` 据此改发 [`crate::dto::PackEvent::Cancelled`]。取消**通常**意味着实例没建
|
||||
//! (管线回滚了),但取消也可能恰好在两个检查点之间落下(那时已经装完并登记)——所以文案
|
||||
//! 只承诺「本次结果作废 + 目标目录已回滚到导入前」,不承诺「一条命令都没执行」。
|
||||
|
||||
mod convert;
|
||||
mod diagnose;
|
||||
@@ -151,7 +152,7 @@ impl Drop for InFlight {
|
||||
/// 取消这个 host 正在跑的导入(`CancelInstall` 与「关面板」都走它;没在跑时是 no-op)。
|
||||
///
|
||||
/// 返回值 = 「刚才确实有一次导入在跑」。调用方据此决定提示语:**导入的取消不是 install 的取消**
|
||||
/// (后台会跑完并登记实例,见模块文档),所以不能复用下载页那条「安装已取消、实例未创建」。
|
||||
/// (M195 起 core 真的停手并回滚,见模块文档),所以不能复用下载页那条「安装已取消、实例未创建」。
|
||||
pub(in crate::host) fn cancel_pack(env: &HostEnv) -> bool {
|
||||
let key = host_key(env);
|
||||
{
|
||||
|
||||
@@ -100,10 +100,21 @@ const fn reason_of(reason: ConflictReason) -> ConflictReasonView {
|
||||
}
|
||||
|
||||
/// 一次导入结果 → DTO。
|
||||
///
|
||||
/// # `Cancelled` 为什么在这里没有对应档(M195)
|
||||
///
|
||||
/// DTO 的 `ImportStatusView` 只有三档,取消走的是 [`crate::dto::PackEvent::Cancelled`]
|
||||
/// (`import::run` 拿到 `ImportStatus::Cancelled` 就不发报告事件了)。所以本函数只会在
|
||||
/// "取消恰好落在两个检查点之间、报告仍然是成功/部分成功"时被调用。真收到 `Cancelled`
|
||||
/// 时按 `Partial` 落地:**绝不是** `Failed`——取消不是失败,不许画成红的。
|
||||
pub(super) fn report_view(report: &ImportReport, target: &Path) -> ImportReportView {
|
||||
ImportReportView {
|
||||
status: ImportStatusView::from_slug(report.status.as_str())
|
||||
.unwrap_or(ImportStatusView::Failed),
|
||||
status: match report.status {
|
||||
ImportStatus::Cancelled => ImportStatusView::Partial,
|
||||
other => {
|
||||
ImportStatusView::from_slug(other.as_str()).unwrap_or(ImportStatusView::Failed)
|
||||
}
|
||||
},
|
||||
format: report.format.as_deref().and_then(PackFormatView::from_slug),
|
||||
pack_name: report.pack_name.clone(),
|
||||
pack_version: report.pack_version.clone(),
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
//! 往下导(不打扰用户);有冲突就回 [`PackEvent::Scan`] 让页面画清单,等用户点完再导;
|
||||
//! 3. **缺省档永远 `KeepAll`**:决策从 DTO 逐字段搬([`convert::decisions_of`]),页面没点过的
|
||||
//! 路径在 core 侧落到"保留";
|
||||
//! 4. **取消后不再打扰界面**:进度回调与最终报告都先看取消标记。
|
||||
//! 4. **取消是真取消**(M195):`cancel` 标记既挡界面事件、又**交给 core 当取消令牌**——管线
|
||||
//! 在检查点停手并回滚,不再"后台跑完并登记"(逐段检查点见
|
||||
//! `core::addon::import::cancel`)。取消后不再打扰界面:进度回调与最终报告都先看取消标记。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use unlucky_core::addon::import::{
|
||||
import_pack, scan_pack_online, DownloadEngineFetcher, ImportProgress, ImportProgressSink,
|
||||
ImportRequest,
|
||||
import_pack, scan_pack_online, DownloadEngineFetcher, ImportCancel, ImportProgress,
|
||||
ImportProgressSink, ImportRequest,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
|
||||
@@ -181,8 +183,11 @@ pub(super) fn run(
|
||||
|
||||
let engine = super::super::super::engine::engine(env);
|
||||
let fetcher = DownloadEngineFetcher::new(&engine);
|
||||
// 这个开关从 M195 起是**真令牌**:core 的每个检查点都看它(不只是挡界面事件)。
|
||||
let token = ImportCancel::from_flag(Arc::clone(&cancel));
|
||||
let mut core_request = ImportRequest::new(&request.pack, &target)
|
||||
.with_fetcher(&fetcher)
|
||||
.with_cancel(&token)
|
||||
.with_progress(sink)
|
||||
// 决策逐字段搬过来:`KeepAll`(缺省)/ `OverwriteAll` / 逐项,语义在 core 那一侧落地。
|
||||
.with_decisions(convert::decisions_of(&request.decisions));
|
||||
@@ -191,15 +196,20 @@ pub(super) fn run(
|
||||
}
|
||||
|
||||
let report = block_on(import_pack(&core_request));
|
||||
if report.status == ImportStatus::Failed && register {
|
||||
// 失败已回滚,但**目录是我们建的**、core 只清文件不清目录 ⇒ 不清掉的话
|
||||
// 下一次重试会撞「directory already exists」(复审 r1 P2-9)。
|
||||
if (report.status == ImportStatus::Failed || report.status == ImportStatus::Cancelled)
|
||||
&& register
|
||||
{
|
||||
// 失败 / 取消都已回滚,但**目录是我们建的**(`prepare_target`)、core 只清文件不清
|
||||
// 目录 ⇒ 不清掉的话下一次重试会撞「directory already exists」(复审 r1 P2-9)。
|
||||
remove_if_empty(&target);
|
||||
}
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
if report.status == ImportStatus::Cancelled || cancel.load(Ordering::SeqCst) {
|
||||
// 取消之后不再打扰界面(面板已经收回,代次也已经作废)。
|
||||
// 注意:后台这一步**已经跑完**(core 没有取消令牌),实例可能已被登记 ——
|
||||
// 所以取消的提示语必须说「还在后台跑」,不能说「实例未创建」。
|
||||
//
|
||||
// M195 起这条是**真的停下来了**:core 在检查点回滚(暂存目录已删,落盘过的已撤回,
|
||||
// 实例没有登记)。所以提示语可以照实说「已取消:目标目录未改动」;
|
||||
// `cancel.load(..)` 单独留一条是因为取消也可能**恰好落在两个检查点之间**(管线已经
|
||||
// 跑完并登记),那时同样不再打扰界面,但别对用户说"没装"。
|
||||
return;
|
||||
}
|
||||
send(
|
||||
|
||||
Reference in new issue
Block a user