updater.rs

  1use std::{
  2    os::windows::process::CommandExt,
  3    path::Path,
  4    time::{Duration, Instant},
  5};
  6
  7use anyhow::{Context as _, Result};
  8use windows::Win32::{
  9    Foundation::{HWND, LPARAM, WPARAM},
 10    System::Threading::CREATE_NEW_PROCESS_GROUP,
 11    UI::WindowsAndMessaging::PostMessageW,
 12};
 13
 14use crate::windows_impl::WM_JOB_UPDATED;
 15
 16type Job = fn(&Path) -> Result<()>;
 17
 18#[cfg(not(test))]
 19pub(crate) const JOBS: &[Job] = &[
 20    // Delete old files
 21    |app_dir| {
 22        let zed_executable = app_dir.join("Zed.exe");
 23        log::info!("Removing old file: {}", zed_executable.display());
 24        std::fs::remove_file(&zed_executable).context(format!(
 25            "Failed to remove old file {}",
 26            zed_executable.display()
 27        ))
 28    },
 29    |app_dir| {
 30        let zed_cli = app_dir.join("bin\\zed.exe");
 31        log::info!("Removing old file: {}", zed_cli.display());
 32        std::fs::remove_file(&zed_cli)
 33            .context(format!("Failed to remove old file {}", zed_cli.display()))
 34    },
 35    |app_dir| {
 36        let zed_wsl = app_dir.join("bin\\zed");
 37        log::info!("Removing old file: {}", zed_wsl.display());
 38        std::fs::remove_file(&zed_wsl)
 39            .context(format!("Failed to remove old file {}", zed_wsl.display()))
 40    },
 41    // Copy new files
 42    |app_dir| {
 43        let zed_executable_source = app_dir.join("install\\Zed.exe");
 44        let zed_executable_dest = app_dir.join("Zed.exe");
 45        log::info!(
 46            "Copying new file {} to {}",
 47            zed_executable_source.display(),
 48            zed_executable_dest.display()
 49        );
 50        std::fs::copy(&zed_executable_source, &zed_executable_dest)
 51            .map(|_| ())
 52            .context(format!(
 53                "Failed to copy new file {} to {}",
 54                zed_executable_source.display(),
 55                zed_executable_dest.display()
 56            ))
 57    },
 58    |app_dir| {
 59        let zed_cli_source = app_dir.join("install\\bin\\zed.exe");
 60        let zed_cli_dest = app_dir.join("bin\\zed.exe");
 61        log::info!(
 62            "Copying new file {} to {}",
 63            zed_cli_source.display(),
 64            zed_cli_dest.display()
 65        );
 66        std::fs::copy(&zed_cli_source, &zed_cli_dest)
 67            .map(|_| ())
 68            .context(format!(
 69                "Failed to copy new file {} to {}",
 70                zed_cli_source.display(),
 71                zed_cli_dest.display()
 72            ))
 73    },
 74    |app_dir| {
 75        let zed_wsl_source = app_dir.join("install\\bin\\zed");
 76        let zed_wsl_dest = app_dir.join("bin\\zed");
 77        log::info!(
 78            "Copying new file {} to {}",
 79            zed_wsl_source.display(),
 80            zed_wsl_dest.display()
 81        );
 82        std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
 83            .map(|_| ())
 84            .context(format!(
 85                "Failed to copy new file {} to {}",
 86                zed_wsl_source.display(),
 87                zed_wsl_dest.display()
 88            ))
 89    },
 90    // Clean up installer folder and updates folder
 91    |app_dir| {
 92        let updates_folder = app_dir.join("updates");
 93        log::info!("Cleaning up: {}", updates_folder.display());
 94        std::fs::remove_dir_all(&updates_folder).context(format!(
 95            "Failed to remove updates folder {}",
 96            updates_folder.display()
 97        ))
 98    },
 99    |app_dir| {
100        let installer_folder = app_dir.join("install");
101        log::info!("Cleaning up: {}", installer_folder.display());
102        std::fs::remove_dir_all(&installer_folder).context(format!(
103            "Failed to remove installer folder {}",
104            installer_folder.display()
105        ))
106    },
107];
108
109#[cfg(test)]
110pub(crate) const JOBS: &[Job] = &[
111    |_| {
112        std::thread::sleep(Duration::from_millis(1000));
113        if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
114            match config.as_str() {
115                "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
116                _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
117            }
118        } else {
119            Ok(())
120        }
121    },
122    |_| {
123        std::thread::sleep(Duration::from_millis(1000));
124        if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
125            match config.as_str() {
126                "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
127                _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
128            }
129        } else {
130            Ok(())
131        }
132    },
133];
134
135pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
136    let hwnd = hwnd.map(|ptr| HWND(ptr as _));
137
138    for job in JOBS.iter() {
139        let start = Instant::now();
140        loop {
141            anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
142            match (*job)(app_dir) {
143                Ok(_) => {
144                    unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
145                    break;
146                }
147                Err(err) => {
148                    // Check if it's a "not found" error
149                    let io_err = err.downcast_ref::<std::io::Error>().unwrap();
150                    if io_err.kind() == std::io::ErrorKind::NotFound {
151                        log::warn!("File or folder not found.");
152                        unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
153                        break;
154                    }
155
156                    log::error!("Operation failed: {}", err);
157                    std::thread::sleep(Duration::from_millis(50));
158                }
159            }
160        }
161    }
162    if launch {
163        #[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
164        let _ = std::process::Command::new(app_dir.join("Zed.exe"))
165            .creation_flags(CREATE_NEW_PROCESS_GROUP.0)
166            .spawn();
167    }
168    log::info!("Update completed successfully");
169    Ok(())
170}
171
172#[cfg(test)]
173mod test {
174    use super::perform_update;
175
176    #[test]
177    fn test_perform_update() {
178        let app_dir = std::path::Path::new("C:/");
179        assert!(perform_update(app_dir, None, false).is_ok());
180
181        // Simulate a timeout
182        unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
183        let ret = perform_update(app_dir, None, false);
184        assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
185    }
186}