fix(m159): r1 复审返工 —— parse.rs 拆出子模块 + 五条 P3 真问题
按独立复审 `reviewer-updategui-r1` 的 p1-1 / fix-then-merge 返工。
P1-1(挡门)
- `crates/cli/src/cli/parse.rs` 406 → **382** 行:`fn parse_update` 搬到
`crates/cli/src/cli/parse/update.rs`(与 `parse/{accounts,agent,instances}.rs` 同形状)。
全仓 `crates/**/*.rs` 最大仍是 400(`design/tests/card_neutral.rs` 等三处,未越线)。
上一轮报告里「所有改动文件都在 400 行内」那句是错的,已在 r2 报告里更正。
P3-6 `Job::apply` 不查包形态
- 新增 `self_replaceable(format)`(只有 `PortableExe` 允许)+ `ensure_self_replaceable`,
**挡在取包之前**;错误分类 `InvalidManifest`(发布侧挂错,换源/重试无用)。
比复审建议的 `needs_installer()` 更严:`portable-zip`/`appimage` 同样不能原地当 exe。
- 用例:`crates/core/src/update/tests/job_cases.rs`(msi/zip 被拒 + 目标零改动 + 暂存目录
没被建 + 便携 exe 整环成功 + Nothing 不碰目标 + 闸门的源码锚点)。
P3-7 `ULL_UPDATE_DIR` 生产可用
- `default_source` 改走 `local_dir_override()`:`if !cfg!(debug_assertions) { return None }`,
发布构建里整段被常量折叠掉(这条路径在 release 二进制里不存在)。判据与三档取舍写在
`job.rs` 的文档表里(为什么不用 CLI 参数、为什么不用运行期开关)。
P3-1「忽略这个版本」语义
- `UpdateView.dismissed: bool` → `skipped: Option<String>`(记**版本号**):忽略 0.2.3 之后
同一次运行里冒出 0.2.4 **照常提示**;手动检查清空(`begin_check(manual)`),静默检查不清。
文案保持「忽略这个版本」。presenter / app 侧视图模型 / 两处用例同步。
P3-3 CLI `--json` 失败信封缺结构化分类
- `CliError` 加 `detail`(`Option<Box<Value>>`,装箱避免 `result_large_err`)+ `with_detail`,
`to_json` 输出 `error.detail`;`commands/update.rs` 的 failure 填
`{"kind": core 分类 id, "retryable": 可重试位}`。
- 上一轮报告里引的 `{"kind":"digest_mismatch"}` 形状确实不存在(那是 CLI 的 user/internal),
r2 报告换成了实机 dump。
P3-4 update 命令没有解析测试
- `crates/cli/tests/parse_test.rs`:`update_subcommands`(check / apply / `--dest` 两种写法)
+ `update_usage_errors`(无子命令 / 未知子命令 / 多余位置参数 / 缺少取值 / 未知选项);
`help_text_covers_every_command` 的 topic 列表补 `"update"`。
P3-5 `install.rs` 模块文档与实测矛盾
- 改成「可以改名、不能覆盖/删除」,写明 M159 在 Windows 真机验过运行中的 exe 自替换,
并提醒别再照旧文档去设计「退出自身 → 另一个进程搬运」的双进程方案。
顺手(复审 P3-2/P3-8 里低成本的两条)
- 设置页补「下载中」态(`SettingsUpdateApplying` 不再是死 key,下载期间不画「下载并安装」);
渠道显示改读 `update.channel`(`stable` 走 key,其它原样);删掉从未使用的
`UpdateBannerRestartLater`(catalog + 金表同步,29 条 key 全部有引用)。
- `adopt` 只让**检查**刷新 `checked_at`(应用回报不再冒充「上次检查」);
`UpdateView::default()` 的 `current_version` 取本进程版本号(mock 下不再空白)。
门禁(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-159`):fmt 绿、clippy -D warnings 绿、
`cargo test --workspace --all-targets --no-fail-fast -j 4` **exit 0**(51 个测试二进制全 ok,
`core::addon::pack` 未被 skip)。该族用例复现了复审记的泄漏:一次全量在 `/tmp` 留 72 个
`ull-pack-*`(`/tmp` 149M → 8853M),跑完已 `rm -rf` 清回 150M。
This commit is contained in:
1 parent
1c8f6aa1b3
commit
04ac90911e
19 files changed
+521
-112
No files matched your search
@@ -38,15 +38,17 @@ use super::checker::{CheckOutcome, UpdateChecker};
|
||||
use super::error::UpdateError;
|
||||
use super::github::GitHubReleaseSource;
|
||||
use super::install::{install_staged, InstallReceipt, ReplacePolicy};
|
||||
use super::manifest::Artifact;
|
||||
use super::source::{DirSource, UpdateSource};
|
||||
use super::target::Target;
|
||||
use super::target::{PackageFormat, Target};
|
||||
use super::verify::stage;
|
||||
use super::version::Version;
|
||||
|
||||
/// 本机更新源覆盖(取证 / 内网手工分发):指向一个 `DirSource` 根目录(`<root>/stable.json`)。
|
||||
///
|
||||
/// 生产不设它——真实网络下 `releases/latest` 是唯一入口。它的用途是把「发现新版本」这条分支
|
||||
/// 在没有稳定 Release 的情况下也能整环验一遍(本地清单 + 本地产物,一个字节都不出网)。
|
||||
/// **只在调试构建里生效**(判据与理由见 [`local_dir_override`]):发布构建忽略它,
|
||||
/// 真实网络下 `releases/latest` 是唯一入口。它的用途是把「发现新版本」这条分支在没有稳定
|
||||
/// Release 的情况下也能整环验一遍(本地清单 + 本地产物,一个字节都不出网)。
|
||||
pub const UPDATE_DIR_ENV: &str = "ULL_UPDATE_DIR";
|
||||
|
||||
/// 一次应用的结局。
|
||||
@@ -165,6 +167,10 @@ impl Job {
|
||||
let Some(candidate) = outcome.candidate() else {
|
||||
return Ok(Applied::Nothing(outcome));
|
||||
};
|
||||
// P3-6:清单只有 `target` 参与挑选(`Release::artifact_for` 只比平台),所以清单可以给
|
||||
// win-x64 挂一个 msi;以前这里会把它直接 rename 成本进程的 exe(D50 明说「不做 MSI」)。
|
||||
// 先按包形态挡一次,**挡在取包之前**:既不浪费下载,也不会在目标路径上留下任何东西。
|
||||
ensure_self_replaceable(&candidate.artifact)?;
|
||||
let staged = stage(&*self.source, &candidate.artifact, &self.staging)?;
|
||||
let version = candidate.release.version.to_string();
|
||||
match install_staged(&staged, &self.dest, ReplacePolicy::default()) {
|
||||
@@ -174,6 +180,32 @@ impl Job {
|
||||
}
|
||||
}
|
||||
|
||||
/// 这个包形态能不能**原地**变成启动器本体。
|
||||
///
|
||||
/// 只有「便携单文件 exe」([`PackageFormat::PortableExe`])可以:
|
||||
///
|
||||
/// | 形态 | 为什么不行 |
|
||||
/// |---|---|
|
||||
/// | `msi` / `dmg` / `deb` / `rpm` | D50 明说「不做 MSI」——那是系统包管理器的活(`needs_installer()`) |
|
||||
/// | `portable-zip` | 是压缩包,不是启动器本体:rename 到 exe 路径只会得到一个跑不起来的文件 |
|
||||
/// | `appimage` | Linux 的可执行镜像,Windows/macOS 上没有意义;且本模块只做「同目录改名换 exe」 |
|
||||
pub(super) fn self_replaceable(format: PackageFormat) -> bool {
|
||||
matches!(format, PackageFormat::PortableExe)
|
||||
}
|
||||
|
||||
/// 包形态不合规就拒绝(错误分类是 `InvalidManifest`:清单挂错了东西,换源/重试都没用)。
|
||||
fn ensure_self_replaceable(artifact: &Artifact) -> Result<(), UpdateError> {
|
||||
if self_replaceable(artifact.format) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(UpdateError::invalid_manifest(format!(
|
||||
"发布里的 {} 产物是 {} 形态,不能原地替换启动器:本启动器只做便携单文件 exe 的自替换\
|
||||
(安装器形态请用系统包管理器;压缩包请解压后再用)",
|
||||
artifact.target.as_str(),
|
||||
artifact.format.as_str()
|
||||
)))
|
||||
}
|
||||
|
||||
/// `&dyn UpdateSource` 也能当源用([`UpdateChecker`] 是泛型的,这里省掉一次装箱)。
|
||||
struct SourceRef<'a>(&'a dyn UpdateSource);
|
||||
|
||||
@@ -210,11 +242,41 @@ pub fn staging_dir(cache_root: &Path) -> PathBuf {
|
||||
cache_root.join("update").join("staging")
|
||||
}
|
||||
|
||||
/// 生产源:GitHub Releases(D50);`ULL_UPDATE_DIR` 设了就走本地目录源。
|
||||
/// 生产源:GitHub Releases(D50);**只在调试构建里**,`ULL_UPDATE_DIR` 设了就走本地目录源。
|
||||
#[must_use]
|
||||
pub fn default_source(engine: Arc<DownloadEngine>) -> Box<dyn UpdateSource> {
|
||||
match std::env::var(UPDATE_DIR_ENV) {
|
||||
Ok(root) if !root.trim().is_empty() => Box::new(DirSource::new(root.trim())),
|
||||
_ => Box::new(GitHubReleaseSource::new(engine)),
|
||||
match local_dir_override() {
|
||||
Some(root) => Box::new(DirSource::new(root)),
|
||||
None => Box::new(GitHubReleaseSource::new(engine)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `ULL_UPDATE_DIR` 的落点:**只认调试构建**。
|
||||
///
|
||||
/// # 为什么判据是 `debug_assertions` 而不是别的
|
||||
///
|
||||
/// 这个后门能绕过 GitHub 源:谁能让启动器读到一份本地清单,谁就能让它把任意字节 rename 成本进程
|
||||
/// 的 exe。清单里的摘要也来自同一份清单,所以**这条路上没有可信根**——它不是权限提升(要能设用户
|
||||
/// 环境变量 + 用户自己点「下载并安装」),但发布产物里没有任何理由保留它。
|
||||
///
|
||||
/// 三档选一,我选了「按构建画像判」:
|
||||
///
|
||||
/// | 判据 | 为什么不选 |
|
||||
/// |---|---|
|
||||
/// | 命令行显式参数(`--update-dir`) | 要给 GUI 与 CLI 各加一条参数、还要穿到 `Job` 里;而它只服务取证/内网,不该出现在正式用法里 |
|
||||
/// | 运行期开关(设置键 / 环境变量再加一层鉴权) | 等于「发布版也留着这条路」,只是多了一道可被一起设好的门 |
|
||||
/// | **`cfg!(debug_assertions)`** | 发布构建(`--release`)里这个常量是 `false`,整段分支被常量折叠掉——**release 二进制里这条路径根本不存在**,而且是编译期事实,不靠用户/环境配合 |
|
||||
///
|
||||
/// 调试构建仍认它:本 mission 的「发现新版本 → 下载 → 双检 → 自替换」整环就是用 `cargo build`
|
||||
/// 出来的二进制 + 本地夹具验的(真实 GitHub 上 v0.2.2 还是 prerelease,取不到 stable 清单)。
|
||||
fn local_dir_override() -> Option<std::path::PathBuf> {
|
||||
if !cfg!(debug_assertions) {
|
||||
return None;
|
||||
}
|
||||
let root = std::env::var(UPDATE_DIR_ENV).ok()?;
|
||||
let root = root.trim();
|
||||
if root.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(std::path::PathBuf::from(root))
|
||||
}
|
||||
Reference in new issue
Block a user