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}