启动后不久检查更新时,下好的新版本在重启时找不到

更新程序启动 30 秒后清理旧的暂存,原来会删掉所有 .ShellRS.app.update-*(Linux 是
.<AppImage>.update-*),包括启动后手动检查、切换渠道刚下载暂存好的新版本,于是「重启并
安装」报找不到文件,重试才从下载目录重新暂存。现在清理只删不比当前版本新的暂存,和下载
目录的规则一致。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-10 23:21:47 +08:00
1 parent db0a0faf05
commit dc5adddd06
6 files changed
+83 -20

No files matched your search

+12 -3
View File
@@ -207,8 +207,9 @@ pub trait Installer: Send + Sync {
/// Remove what earlier updates left next to the installed copy. Called a /// Remove what earlier updates left next to the installed copy. Called a
/// while after start, so a version that crashes right away leaves the /// while after start, so a version that crashes right away leaves the
/// previous one's backup behind. /// previous one's backup behind. An update newer than `current` stays:
fn clean_up(&self); /// it may have been staged since start and be waiting for the restart.
fn clean_up(&self, current: &Version);
} }
/// For copies that cannot replace themselves. /// For copies that cannot replace themselves.
@@ -237,7 +238,7 @@ impl Installer for NoInstaller {
)) ))
} }
fn clean_up(&self) {} fn clean_up(&self, _: &Version) {}
} }
/// The installer for this copy of ShellRS. `bundle` is `cx.app_path()`. /// The installer for this copy of ShellRS. `bundle` is `cx.app_path()`.
@@ -296,6 +297,14 @@ pub(crate) fn sibling(path: &Path, name: &str) -> PathBuf {
path.with_file_name(name) path.with_file_name(name)
} }
/// Whether `name` is an update staged as `<prefix><version>` that this copy
/// no longer needs: one not newer than `current`, or not named by a version.
#[cfg(any(target_os = "macos", target_os = "linux", test))]
pub(crate) fn is_stale_update(name: &str, prefix: &str, current: &Version) -> bool {
name.strip_prefix(prefix)
.is_some_and(|version| Version::parse(version).map_or(true, |version| version <= *current))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+22 -7
View File
@@ -10,7 +10,9 @@ use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::error::UpdateError; use super::error::UpdateError;
use super::install::{InstallKind, Installer, Relaunch, Staged, sibling}; use semver::Version;
use super::install::{InstallKind, Installer, Relaunch, Staged, is_stale_update, sibling};
use super::manifest::Release; use super::manifest::Release;
pub struct AppImageInstaller { pub struct AppImageInstaller {
@@ -59,13 +61,13 @@ impl Installer for AppImageInstaller {
}) })
} }
fn clean_up(&self) { fn clean_up(&self, current: &Version) {
let Some(folder) = self.file.parent() else { let Some(folder) = self.file.parent() else {
return; return;
}; };
let prefix = self.prefix(); let prefix = self.prefix();
for entry in fs::read_dir(folder).into_iter().flatten().flatten() { for entry in fs::read_dir(folder).into_iter().flatten().flatten() {
if entry.file_name().to_string_lossy().starts_with(&prefix) { if is_stale_update(&entry.file_name().to_string_lossy(), &prefix, current) {
let _ = fs::remove_file(entry.path()); let _ = fs::remove_file(entry.path());
} }
} }
@@ -115,13 +117,26 @@ mod tests {
"stale", "stale",
) )
.unwrap(); .unwrap();
installer.clean_up(); // Staged since start and waiting for the restart: kept.
let left: Vec<_> = fs::read_dir(folder.path()) fs::write(
sibling(&file, ".ShellRS-x86_64.AppImage.update-0.3.0"),
"next",
)
.unwrap();
installer.clean_up(&Version::new(0, 2, 0));
let mut left: Vec<_> = fs::read_dir(folder.path())
.unwrap() .unwrap()
.flatten() .flatten()
.map(|entry| entry.file_name().to_string_lossy().into_owned()) .map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect(); .collect();
assert_eq!(left.len(), 2, "{left:?}"); left.sort();
assert!(left.contains(&"ShellRS-x86_64.AppImage".to_string())); assert_eq!(
left,
[
".ShellRS-x86_64.AppImage.update-0.3.0",
"ShellRS-x86_64.AppImage",
"downloaded"
]
);
} }
} }
+43 -7
View File
@@ -14,9 +14,11 @@ use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::sync::Arc; use std::sync::Arc;
use semver::Version;
use super::build_info::APPLE_TEAM_ID; use super::build_info::APPLE_TEAM_ID;
use super::error::UpdateError; use super::error::UpdateError;
use super::install::{InstallKind, Installer, Relaunch, Staged, sibling}; use super::install::{InstallKind, Installer, Relaunch, Staged, is_stale_update, sibling};
use super::manifest::Release; use super::manifest::Release;
use crate::i18n::t; use crate::i18n::t;
@@ -217,7 +219,7 @@ impl Installer for MacInstaller {
}) })
} }
fn clean_up(&self) { fn clean_up(&self, current: &Version) {
let Some(folder) = self.bundle.parent() else { let Some(folder) = self.bundle.parent() else {
return; return;
}; };
@@ -227,7 +229,7 @@ impl Installer for MacInstaller {
); );
for entry in fs::read_dir(folder).into_iter().flatten().flatten() { for entry in fs::read_dir(folder).into_iter().flatten().flatten() {
let name = entry.file_name().to_string_lossy().into_owned(); let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with(&old) || name.starts_with(&update) { if name.starts_with(&old) || is_stale_update(&name, &update, current) {
let _ = fs::remove_dir_all(entry.path()); let _ = fs::remove_dir_all(entry.path());
} }
} }
@@ -349,10 +351,40 @@ mod tests {
assert_eq!(names.len(), 2, "{names:?}"); assert_eq!(names.len(), 2, "{names:?}");
assert!(names[0].starts_with(".ShellRS.app.old-"), "{names:?}"); assert!(names[0].starts_with(".ShellRS.app.old-"), "{names:?}");
installer.clean_up(); // Run by 0.2.0, once it has started.
installer.clean_up(&Version::new(0, 2, 0));
assert_eq!(names_in(folder.path()), ["ShellRS.app"]); assert_eq!(names_in(folder.path()), ["ShellRS.app"]);
} }
/// The clean-up runs a while after start; an update downloaded and
/// staged before then is still there to install on restart.
#[test]
fn the_clean_up_keeps_an_update_staged_for_the_restart() {
let (folder, bundle) = installed();
let installer = MacInstaller::new(bundle.clone(), FakeTools::new("0.2.0", None));
for stale in [
".ShellRS.app.update-0.1.0",
".ShellRS.app.update-later",
".ShellRS.app.old-1",
] {
fs::create_dir(folder.path().join(stale)).unwrap();
}
let staged = installer
.stage(Path::new("package.zip"), &release("0.2.0"))
.unwrap();
installer.clean_up(&Version::new(0, 1, 0));
assert_eq!(
names_in(folder.path()),
[".ShellRS.app.update-0.2.0", "ShellRS.app"]
);
assert_eq!(
installer.apply(&staged, true),
Ok(Relaunch::Restart(bundle.clone()))
);
assert_eq!(version_at(&bundle), "0.2.0");
}
#[test] #[test]
fn a_failed_swap_puts_the_old_bundle_back() { fn a_failed_swap_puts_the_old_bundle_back() {
let (folder, bundle) = installed(); let (folder, bundle) = installed();
@@ -367,8 +399,12 @@ mod tests {
Err(UpdateError::Install(_)) Err(UpdateError::Install(_))
)); ));
assert_eq!(version_at(&bundle), "0.1.0"); assert_eq!(version_at(&bundle), "0.1.0");
installer.clean_up(); installer.clean_up(&Version::new(0, 1, 0));
assert_eq!(names_in(folder.path()), ["ShellRS.app"]); assert!(
!names_in(folder.path())
.iter()
.any(|name| name.starts_with(".ShellRS.app.old-"))
);
} }
#[test] #[test]
@@ -484,7 +520,7 @@ mod tests {
.check_signature(&bundle, Some("SHELLRS")) .check_signature(&bundle, Some("SHELLRS"))
.is_err() .is_err()
); );
installer.clean_up(); installer.clean_up(&Version::new(0, 2, 0));
assert_eq!(names_in(&installed), ["ShellRS.app"]); assert_eq!(names_in(&installed), ["ShellRS.app"]);
} }
} }
+4 -1
View File
@@ -11,6 +11,8 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use semver::Version;
use super::error::UpdateError; use super::error::UpdateError;
use super::install::{InstallKind, Installer, Launch, Relaunch, Staged}; use super::install::{InstallKind, Installer, Launch, Relaunch, Staged};
use super::manifest::Release; use super::manifest::Release;
@@ -99,7 +101,8 @@ impl Installer for SetupInstaller {
} }
/// The setup program removes what the previous version left. /// The setup program removes what the previous version left.
fn clean_up(&self) {} // The package waits in the downloads folder, which the updater tidies.
fn clean_up(&self, _: &Version) {}
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -507,7 +507,7 @@ impl Updater {
return; return;
}; };
cx.background_spawn(async move { cx.background_spawn(async move {
services.installer.clean_up(); services.installer.clean_up(&services.current);
clean_downloads(&services.folder, &services.current); clean_downloads(&services.folder, &services.current);
}) })
.detach(); .detach();
+1 -1
View File
@@ -99,7 +99,7 @@ impl Installer for FakeInstaller {
}) })
} }
fn clean_up(&self) {} fn clean_up(&self, _: &Version) {}
} }
/// A copy of ShellRS that can install updates itself. /// A copy of ShellRS that can install updates itself.