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