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; 6] = [
 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    // Copy new files
 36    |app_dir| {
 37        let zed_executable_source = app_dir.join("install\\Zed.exe");
 38        let zed_executable_dest = app_dir.join("Zed.exe");
 39        log::info!(
 40            "Copying new file {} to {}",
 41            zed_executable_source.display(),
 42            zed_executable_dest.display()
 43        );
 44        std::fs::copy(&zed_executable_source, &zed_executable_dest)
 45            .map(|_| ())
 46            .context(format!(
 47                "Failed to copy new file {} to {}",
 48                zed_executable_source.display(),
 49                zed_executable_dest.display()
 50            ))
 51    },
 52    |app_dir| {
 53        let zed_cli_source = app_dir.join("install\\bin\\zed.exe");
 54        let zed_cli_dest = app_dir.join("bin\\zed.exe");
 55        log::info!(
 56            "Copying new file {} to {}",
 57            zed_cli_source.display(),
 58            zed_cli_dest.display()
 59        );
 60        std::fs::copy(&zed_cli_source, &zed_cli_dest)
 61            .map(|_| ())
 62            .context(format!(
 63                "Failed to copy new file {} to {}",
 64                zed_cli_source.display(),
 65                zed_cli_dest.display()
 66            ))
 67    },
 68    // Clean up installer folder and updates folder
 69    |app_dir| {
 70        let updates_folder = app_dir.join("updates");
 71        log::info!("Cleaning up: {}", updates_folder.display());
 72        std::fs::remove_dir_all(&updates_folder).context(format!(
 73            "Failed to remove updates folder {}",
 74            updates_folder.display()
 75        ))
 76    },
 77    |app_dir| {
 78        let installer_folder = app_dir.join("install");
 79        log::info!("Cleaning up: {}", installer_folder.display());
 80        std::fs::remove_dir_all(&installer_folder).context(format!(
 81            "Failed to remove installer folder {}",
 82            installer_folder.display()
 83        ))
 84    },
 85];
 86
 87#[cfg(test)]
 88pub(crate) const JOBS: [Job; 2] = [
 89    |_| {
 90        std::thread::sleep(Duration::from_millis(1000));
 91        if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
 92            match config.as_str() {
 93                "err" => Err(std::io::Error::new(
 94                    std::io::ErrorKind::Other,
 95                    "Simulated error",
 96                ))
 97                .context("Anyhow!"),
 98                _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
 99            }
100        } else {
101            Ok(())
102        }
103    },
104    |_| {
105        std::thread::sleep(Duration::from_millis(1000));
106        if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
107            match config.as_str() {
108                "err" => Err(std::io::Error::new(
109                    std::io::ErrorKind::Other,
110                    "Simulated error",
111                ))
112                .context("Anyhow!"),
113                _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
114            }
115        } else {
116            Ok(())
117        }
118    },
119];
120
121pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
122    let hwnd = hwnd.map(|ptr| HWND(ptr as _));
123
124    for job in JOBS.iter() {
125        let start = Instant::now();
126        loop {
127            anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
128            match (*job)(app_dir) {
129                Ok(_) => {
130                    unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
131                    break;
132                }
133                Err(err) => {
134                    // Check if it's a "not found" error
135                    let io_err = err.downcast_ref::<std::io::Error>().unwrap();
136                    if io_err.kind() == std::io::ErrorKind::NotFound {
137                        log::warn!("File or folder not found.");
138                        unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
139                        break;
140                    }
141
142                    log::error!("Operation failed: {}", err);
143                    std::thread::sleep(Duration::from_millis(50));
144                }
145            }
146        }
147    }
148    if launch {
149        let _ = std::process::Command::new(app_dir.join("Zed.exe"))
150            .creation_flags(CREATE_NEW_PROCESS_GROUP.0)
151            .spawn();
152    }
153    log::info!("Update completed successfully");
154    Ok(())
155}
156
157#[cfg(test)]
158mod test {
159    use super::perform_update;
160
161    #[test]
162    fn test_perform_update() {
163        let app_dir = std::path::Path::new("C:/");
164        assert!(perform_update(app_dir, None, false).is_ok());
165
166        // Simulate a timeout
167        unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
168        let ret = perform_update(app_dir, None, false);
169        assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
170    }
171}