updater.rs

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