Cargo.lock 🔗
@@ -1339,6 +1339,7 @@ dependencies = [
"settings",
"smol",
"tempfile",
+ "util",
"which 6.0.3",
"workspace",
]
Kate created
Cargo.lock | 1
crates/auto_update/Cargo.toml | 1
crates/auto_update/src/auto_update.rs | 30 +
crates/auto_update_helper/src/updater.rs | 465 +++++++++++++++----------
4 files changed, 307 insertions(+), 190 deletions(-)
@@ -1339,6 +1339,7 @@ dependencies = [
"settings",
"smol",
"tempfile",
+ "util",
"which 6.0.3",
"workspace",
]
@@ -27,6 +27,7 @@ settings.workspace = true
smol.workspace = true
tempfile.workspace = true
workspace.workspace = true
+util.workspace = true
[target.'cfg(not(target_os = "windows"))'.dependencies]
which.workspace = true
@@ -24,6 +24,7 @@ use std::{
sync::Arc,
time::Duration,
};
+use util::ResultExt;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -331,6 +332,12 @@ impl AutoUpdater {
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
+ #[cfg(windows)]
+ cleanup_windows()
+ .await
+ .context("failed to cleanup old directories")
+ .log_err();
+
loop {
this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
@@ -923,6 +930,29 @@ async fn install_release_macos(
Ok(None)
}
+async fn cleanup_windows() -> Result<()> {
+ let parent = std::env::current_exe()?
+ .parent()
+ .context("No parent dir for Zed.exe")?
+ .to_owned();
+
+ // keep in sync with crates/auto_update_helper/src/updater.rs
+ smol::fs::remove_dir(parent.join("updates"))
+ .await
+ .context("failed to remove updates dir")
+ .log_err();
+ smol::fs::remove_dir(parent.join("install"))
+ .await
+ .context("failed to remove install dir")
+ .log_err();
+ smol::fs::remove_dir(parent.join("old"))
+ .await
+ .context("failed to remove old version dir")
+ .log_err();
+
+ Ok(())
+}
+
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
let output = Command::new(downloaded_installer)
.arg("/verysilent")
@@ -1,4 +1,5 @@
use std::{
+ cell::LazyCell,
path::Path,
time::{Duration, Instant},
};
@@ -11,210 +12,255 @@ use windows::Win32::{
use crate::windows_impl::WM_JOB_UPDATED;
-type Job = fn(&Path) -> Result<()>;
+pub(crate) struct Job {
+ pub apply: Box<dyn Fn(&Path) -> Result<()>>,
+ pub rollback: Box<dyn Fn(&Path) -> Result<()>>,
+}
-#[cfg(not(test))]
-pub(crate) const JOBS: &[Job] = &[
- // Delete old files
- |app_dir| {
- let zed_executable = app_dir.join("Zed.exe");
- log::info!("Removing old file: {}", zed_executable.display());
- std::fs::remove_file(&zed_executable).context(format!(
- "Failed to remove old file {}",
- zed_executable.display()
- ))
- },
- |app_dir| {
- let zed_cli = app_dir.join("bin\\zed.exe");
- log::info!("Removing old file: {}", zed_cli.display());
- std::fs::remove_file(&zed_cli)
- .context(format!("Failed to remove old file {}", zed_cli.display()))
- },
- |app_dir| {
- let zed_wsl = app_dir.join("bin\\zed");
- log::info!("Removing old file: {}", zed_wsl.display());
- std::fs::remove_file(&zed_wsl)
- .context(format!("Failed to remove old file {}", zed_wsl.display()))
- },
- // TODO: remove after a few weeks once everyone is on the new version and this file never exists
- |app_dir| {
- let open_console = app_dir.join("OpenConsole.exe");
- if open_console.exists() {
- log::info!("Removing old file: {}", open_console.display());
- std::fs::remove_file(&open_console).context(format!(
- "Failed to remove old file {}",
- open_console.display()
- ))?
+impl Job {
+ pub fn mkdir(name: &'static Path) -> Self {
+ Job {
+ apply: Box::new(move |app_dir| {
+ let dir = app_dir.join(name);
+ std::fs::create_dir_all(&dir)
+ .context(format!("Failed to create directory {}", dir.display()))
+ }),
+ rollback: Box::new(move |app_dir| {
+ let dir = app_dir.join(name);
+ std::fs::remove_dir_all(&dir)
+ .context(format!("Failed to remove directory {}", dir.display()))
+ }),
}
- Ok(())
- },
- |app_dir| {
- let archs = ["x64", "arm64"];
- for arch in archs {
- let open_console = app_dir.join(format!("{arch}\\OpenConsole.exe"));
- if open_console.exists() {
- log::info!("Removing old file: {}", open_console.display());
- std::fs::remove_file(&open_console).context(format!(
- "Failed to remove old file {}",
- open_console.display()
- ))?
- }
+ }
+
+ pub fn mkdir_if_exists(name: &'static Path, check: &'static Path) -> Self {
+ Job {
+ apply: Box::new(move |app_dir| {
+ let dir = app_dir.join(name);
+ let check = app_dir.join(check);
+
+ if check.exists() {
+ std::fs::create_dir_all(&dir)
+ .context(format!("Failed to create directory {}", dir.display()))?
+ }
+ Ok(())
+ }),
+ rollback: Box::new(move |app_dir| {
+ let dir = app_dir.join(name);
+
+ if dir.exists() {
+ std::fs::remove_dir_all(&dir)
+ .context(format!("Failed to remove directory {}", dir.display()))?
+ }
+
+ Ok(())
+ }),
}
- Ok(())
- },
- |app_dir| {
- let conpty = app_dir.join("conpty.dll");
- log::info!("Removing old file: {}", conpty.display());
- std::fs::remove_file(&conpty)
- .context(format!("Failed to remove old file {}", conpty.display()))
- },
- // Copy new files
- |app_dir| {
- let zed_executable_source = app_dir.join("install\\Zed.exe");
- let zed_executable_dest = app_dir.join("Zed.exe");
- log::info!(
- "Copying new file {} to {}",
- zed_executable_source.display(),
- zed_executable_dest.display()
- );
- std::fs::copy(&zed_executable_source, &zed_executable_dest)
- .map(|_| ())
- .context(format!(
- "Failed to copy new file {} to {}",
- zed_executable_source.display(),
- zed_executable_dest.display()
- ))
- },
- |app_dir| {
- let zed_cli_source = app_dir.join("install\\bin\\zed.exe");
- let zed_cli_dest = app_dir.join("bin\\zed.exe");
- log::info!(
- "Copying new file {} to {}",
- zed_cli_source.display(),
- zed_cli_dest.display()
- );
- std::fs::copy(&zed_cli_source, &zed_cli_dest)
- .map(|_| ())
- .context(format!(
- "Failed to copy new file {} to {}",
- zed_cli_source.display(),
- zed_cli_dest.display()
- ))
- },
- |app_dir| {
- let zed_wsl_source = app_dir.join("install\\bin\\zed");
- let zed_wsl_dest = app_dir.join("bin\\zed");
- log::info!(
- "Copying new file {} to {}",
- zed_wsl_source.display(),
- zed_wsl_dest.display()
- );
- std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
- .map(|_| ())
- .context(format!(
- "Failed to copy new file {} to {}",
- zed_wsl_source.display(),
- zed_wsl_dest.display()
- ))
- },
- |app_dir| {
- let archs = ["x64", "arm64"];
- for arch in archs {
- let open_console_source = app_dir.join(format!("install\\{arch}\\OpenConsole.exe"));
- let open_console_dest = app_dir.join(format!("{arch}\\OpenConsole.exe"));
- if open_console_source.exists() {
+ }
+
+ pub fn move_file(filename: &'static Path, new_filename: &'static Path) -> Self {
+ Job {
+ apply: Box::new(move |app_dir| {
+ let old_file = app_dir.join(filename);
+ let new_file = app_dir.join(new_filename);
log::info!(
- "Copying new file {} to {}",
- open_console_source.display(),
- open_console_dest.display()
+ "Moving file: {}->{}",
+ old_file.display(),
+ new_file.display()
);
- let parent = open_console_dest.parent().context(format!(
- "Failed to get parent directory of {}",
- open_console_dest.display()
- ))?;
- std::fs::create_dir_all(parent)
- .context(format!("Failed to create directory {}", parent.display()))?;
- std::fs::copy(&open_console_source, &open_console_dest)
- .map(|_| ())
- .context(format!(
- "Failed to copy new file {} to {}",
- open_console_source.display(),
- open_console_dest.display()
- ))?
- }
+
+ std::fs::rename(&old_file, new_file)
+ .context(format!("Failed to move file {}", old_file.display()))
+ }),
+ rollback: Box::new(move |app_dir| {
+ let old_file = app_dir.join(filename);
+ let new_file = app_dir.join(new_filename);
+ log::info!(
+ "Rolling back file move: {}->{}",
+ old_file.display(),
+ new_file.display()
+ );
+
+ std::fs::rename(&new_file, &old_file).context(format!(
+ "Failed to rollback file move {}->{}",
+ new_file.display(),
+ old_file.display()
+ ))
+ }),
}
- Ok(())
- },
- |app_dir| {
- let conpty_source = app_dir.join("install\\conpty.dll");
- let conpty_dest = app_dir.join("conpty.dll");
- log::info!(
- "Copying new file {} to {}",
- conpty_source.display(),
- conpty_dest.display()
- );
- std::fs::copy(&conpty_source, &conpty_dest)
- .map(|_| ())
- .context(format!(
- "Failed to copy new file {} to {}",
- conpty_source.display(),
- conpty_dest.display()
- ))
- },
- // Clean up installer folder and updates folder
- |app_dir| {
- let updates_folder = app_dir.join("updates");
- log::info!("Cleaning up: {}", updates_folder.display());
- std::fs::remove_dir_all(&updates_folder).context(format!(
- "Failed to remove updates folder {}",
- updates_folder.display()
- ))
- },
- |app_dir| {
- let installer_folder = app_dir.join("install");
- log::info!("Cleaning up: {}", installer_folder.display());
- std::fs::remove_dir_all(&installer_folder).context(format!(
- "Failed to remove installer folder {}",
- installer_folder.display()
- ))
- },
-];
+ }
-#[cfg(test)]
-pub(crate) const JOBS: &[Job] = &[
- |_| {
- std::thread::sleep(Duration::from_millis(1000));
- if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
- match config.as_str() {
- "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
- _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
- }
- } else {
- Ok(())
+ pub fn move_if_exists(filename: &'static Path, new_filename: &'static Path) -> Self {
+ Job {
+ apply: Box::new(move |app_dir| {
+ let old_file = app_dir.join(filename);
+ let new_file = app_dir.join(new_filename);
+
+ if old_file.exists() {
+ log::info!(
+ "Moving file: {}->{}",
+ old_file.display(),
+ new_file.display()
+ );
+
+ std::fs::rename(&old_file, new_file)
+ .context(format!("Failed to move file {}", old_file.display()))?;
+ }
+
+ Ok(())
+ }),
+ rollback: Box::new(move |app_dir| {
+ let old_file = app_dir.join(filename);
+ let new_file = app_dir.join(new_filename);
+
+ if new_file.exists() {
+ log::info!(
+ "Rolling back file move: {}->{}",
+ old_file.display(),
+ new_file.display()
+ );
+
+ std::fs::rename(&new_file, &old_file).context(format!(
+ "Failed to rollback file move {}->{}",
+ new_file.display(),
+ old_file.display()
+ ))?
+ }
+
+ Ok(())
+ }),
}
- },
- |_| {
- std::thread::sleep(Duration::from_millis(1000));
- if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
- match config.as_str() {
- "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
- _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
- }
- } else {
- Ok(())
+ }
+
+ pub fn rmdir_nofail(filename: &'static Path) -> Self {
+ Job {
+ apply: Box::new(move |app_dir| {
+ let filename = app_dir.join(filename);
+ log::info!("Removing file: {}", filename.display());
+ if let Err(e) = std::fs::remove_dir_all(&filename) {
+ log::warn!("Failed to remove directory: {}", e);
+ }
+
+ Ok(())
+ }),
+ rollback: Box::new(move |app_dir| {
+ let filename = app_dir.join(filename);
+ anyhow::bail!(
+ "Delete operations cannot be rolled back, file: {}",
+ filename.display()
+ )
+ }),
}
- },
-];
+ }
+}
+
+// app is single threaded
+#[cfg(not(test))]
+#[allow(clippy::declare_interior_mutable_const)]
+pub(crate) const JOBS: LazyCell<[Job; 22]> = LazyCell::new(|| {
+ fn p(value: &str) -> &Path {
+ Path::new(value)
+ }
+ [
+ // Move old files
+ // Not deleting because installing new files can fail
+ Job::mkdir(p("old")),
+ Job::move_file(p("Zed.exe"), p("old\\Zed.exe")),
+ Job::mkdir(p("old\\bin")),
+ Job::move_file(p("bin\\Zed.exe"), p("old\\bin\\Zed.exe")),
+ Job::move_file(p("bin\\zed"), p("old\\bin\\zed")),
+ //
+ // TODO: remove after a few weeks once everyone is on the new version and this file never exists
+ Job::move_if_exists(p("OpenConsole.exe"), p("old\\OpenConsole.exe")),
+ Job::mkdir(p("old\\x64")),
+ Job::mkdir(p("old\\arm64")),
+ Job::move_if_exists(p("x64\\OpenConsole.exe"), p("old\\x64\\OpenConsole.exe")),
+ Job::move_if_exists(
+ p("arm64\\OpenConsole.exe"),
+ p("old\\arm64\\OpenConsole.exe"),
+ ),
+ //
+ Job::move_file(p("conpty.dll"), p("old\\conpty.dll")),
+ // Copy new files
+ Job::move_file(p("install\\Zed.exe"), p("Zed.exe")),
+ Job::move_file(p("install\\bin\\Zed.exe"), p("bin\\Zed.exe")),
+ Job::move_file(p("install\\bin\\zed"), p("bin\\zed")),
+ //
+ Job::mkdir_if_exists(p("x64"), p("install\\x64")),
+ Job::mkdir_if_exists(p("arm64"), p("install\\arm64")),
+ Job::move_if_exists(
+ p("install\\x64\\OpenConsole.exe"),
+ p("x64\\OpenConsole.exe"),
+ ),
+ Job::move_if_exists(
+ p("install\\arm64\\OpenConsole.exe"),
+ p("arm64\\OpenConsole.exe"),
+ ),
+ //
+ Job::move_file(p("install\\conpty.dll"), p("conpty.dll")),
+ // Cleanup installer and updates folder
+ Job::rmdir_nofail(p("updates")),
+ Job::rmdir_nofail(p("install")),
+ // Cleanup old installation
+ Job::rmdir_nofail(p("old")),
+ ]
+});
+
+// app is single threaded
+#[cfg(test)]
+#[allow(clippy::declare_interior_mutable_const)]
+pub(crate) const JOBS: LazyCell<[Job; 2]> = LazyCell::new(|| {
+ [
+ Job {
+ apply: Box::new(|_| {
+ std::thread::sleep(Duration::from_millis(1000));
+ if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
+ match config.as_str() {
+ "err1" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
+ "err2" => Ok(()),
+ _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
+ }
+ } else {
+ Ok(())
+ }
+ }),
+ rollback: Box::new(|_| {
+ unsafe { std::env::set_var("ZED_AUTO_UPDATE_RB", "rollback1") };
+ Ok(())
+ }),
+ },
+ Job {
+ apply: Box::new(|_| {
+ std::thread::sleep(Duration::from_millis(1000));
+ if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
+ match config.as_str() {
+ "err1" => Ok(()),
+ "err2" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
+ _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
+ }
+ } else {
+ Ok(())
+ }
+ }),
+ rollback: Box::new(|_| Ok(())),
+ },
+ ]
+});
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
let hwnd = hwnd.map(|ptr| HWND(ptr as _));
- for job in JOBS.iter() {
+ let mut last_successfull_job = None;
+ 'outer: for (i, job) in JOBS.iter().enumerate() {
let start = Instant::now();
loop {
- anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
- match (*job)(app_dir) {
+ if start.elapsed().as_secs() > 2 {
+ log::error!("Timed out, rolling back");
+ break 'outer;
+ }
+ match (job.apply)(app_dir) {
Ok(_) => {
+ last_successfull_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
@@ -223,6 +269,7 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
let io_err = err.downcast_ref::<std::io::Error>().unwrap();
if io_err.kind() == std::io::ErrorKind::NotFound {
log::warn!("File or folder not found.");
+ last_successfull_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
@@ -233,6 +280,28 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
}
}
}
+
+ if last_successfull_job
+ .map(|job| job != JOBS.len() - 1)
+ .unwrap_or(true)
+ {
+ let Some(last_successfull_job) = last_successfull_job else {
+ anyhow::bail!("Autoupdate failed, nothing to rollback");
+ };
+
+ for job in (0..=last_successfull_job).rev() {
+ let job = &JOBS[job];
+ if let Err(e) = (job.rollback)(app_dir) {
+ anyhow::bail!(
+ "Job rollback failed, the app might be left in an inconsistent state: ({:?})",
+ e
+ );
+ }
+ }
+
+ anyhow::bail!("Autoupdate failed, rollback successful");
+ }
+
if launch {
#[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
let _ = std::process::Command::new(app_dir.join("Zed.exe")).spawn();
@@ -251,8 +320,24 @@ mod test {
assert!(perform_update(app_dir, None, false).is_ok());
// Simulate a timeout
- unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
+ unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err1") };
let ret = perform_update(app_dir, None, false);
- assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
+ assert!(
+ ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, nothing to rollback")
+ );
+ }
+
+ #[test]
+ fn test_perform_update_rollback() {
+ let app_dir = std::path::Path::new("C:/");
+ assert!(perform_update(app_dir, None, false).is_ok());
+
+ // Simulate a timeout
+ unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err2") };
+ let ret = perform_update(app_dir, None, false);
+ assert!(
+ ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, rollback successful")
+ );
+ assert!(std::env::var("ZED_AUTO_UPDATE_RB").is_ok_and(|e| e == "rollback1"));
}
}