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