diff --git a/Cargo.lock b/Cargo.lock index 78c972865a4e01ba66357142ff8737b634639b27..98921c43115f317d18f9da8214109731120d86af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1339,6 +1339,7 @@ dependencies = [ "settings", "smol", "tempfile", + "util", "which 6.0.3", "workspace", ] diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 08db9f8a97bb0783da987f84991ad1aaa62c2141..772869cfb5b1f8fa5ef0c8c75e9cc0d0ca673642 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -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 diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 9f93dd27900e4b90de8c6d61d41b3b6c287eaaf0..3b251f508708e350ac9853110f18ebc96646c508 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -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) -> Task> { 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> { let output = Command::new(downloaded_installer) .arg("/verysilent") diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index bbf385ecc870cb7630bb109cda4db19cd33c07ec..1586efda6d46a943115a93736ee62ca67ef9885e 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -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 Result<()>>, + pub rollback: Box 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, 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, launch: bool) let io_err = err.downcast_ref::().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, 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")); } }