windows: Implement `AutoUpdater` (#25734)

张小白 created

Part of #24800



https://github.com/user-attachments/assets/e70d594e-3635-4f93-9073-5abf7e9d2b20



Release Notes:

- N/A

Change summary

Cargo.lock                                          |  14 
Cargo.toml                                          |  12 
crates/auto_update/Cargo.toml                       |   4 
crates/auto_update/src/auto_update.rs               | 101 +++++
crates/auto_update_helper/Cargo.toml                |  29 +
crates/auto_update_helper/LICENSE-GPL               |   1 
crates/auto_update_helper/app-icon.ico              |   0 
crates/auto_update_helper/build.rs                  |  15 
crates/auto_update_helper/manifest.xml              |  16 +
crates/auto_update_helper/src/auto_update_helper.rs |  94 +++++
crates/auto_update_helper/src/dialog.rs             | 236 +++++++++++++++
crates/auto_update_helper/src/updater.rs            | 171 ++++++++++
crates/zed/src/main.rs                              |  51 ++-
crates/zed/src/zed/windows_only_instance.rs         |  10 
tooling/workspace-hack/Cargo.toml                   |   4 
15 files changed, 721 insertions(+), 37 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1182,6 +1182,18 @@ dependencies = [
  "workspace-hack",
 ]
 
+[[package]]
+name = "auto_update_helper"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "log",
+ "simplelog",
+ "windows 0.61.1",
+ "winresource",
+ "workspace-hack",
+]
+
 [[package]]
 name = "auto_update_ui"
 version = "0.1.0"
@@ -17767,6 +17779,8 @@ dependencies = [
  "wasmtime-cranelift",
  "wasmtime-environ",
  "winapi",
+ "windows-core 0.61.0",
+ "windows-numerics",
  "windows-sys 0.48.0",
  "windows-sys 0.52.0",
  "windows-sys 0.59.0",

Cargo.toml 🔗

@@ -15,6 +15,7 @@ members = [
     "crates/assistant_tools",
     "crates/audio",
     "crates/auto_update",
+    "crates/auto_update_helper",
     "crates/auto_update_ui",
     "crates/aws_http_client",
     "crates/bedrock",
@@ -222,6 +223,7 @@ assistant_tool = { path = "crates/assistant_tool" }
 assistant_tools = { path = "crates/assistant_tools" }
 audio = { path = "crates/audio" }
 auto_update = { path = "crates/auto_update" }
+auto_update_helper = { path = "crates/auto_update_helper" }
 auto_update_ui = { path = "crates/auto_update_ui" }
 aws_http_client = { path = "crates/aws_http_client" }
 bedrock = { path = "crates/bedrock" }
@@ -782,4 +784,12 @@ let_underscore_future = "allow"
 too_many_arguments = "allow"
 
 [workspace.metadata.cargo-machete]
-ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme", "workspace-hack"]
+ignored = [
+    "bindgen",
+    "cbindgen",
+    "prost_build",
+    "serde",
+    "component",
+    "linkme",
+    "workspace-hack",
+]

crates/auto_update/Cargo.toml 🔗

@@ -27,6 +27,8 @@ serde_json.workspace = true
 settings.workspace = true
 smol.workspace = true
 tempfile.workspace = true
-which.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
+
+[target.'cfg(not(target_os = "windows"))'.dependencies]
+which.workspace = true

crates/auto_update/src/auto_update.rs 🔗

@@ -23,7 +23,6 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use which::which;
 use workspace::Workspace;
 
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -63,7 +62,7 @@ pub struct AutoUpdater {
     pending_poll: Option<Task<Option<()>>>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Debug)]
 pub struct JsonRelease {
     pub version: String,
     pub url: String,
@@ -237,6 +236,46 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
     None
 }
 
+#[cfg(not(target_os = "windows"))]
+struct InstallerDir(tempfile::TempDir);
+
+#[cfg(not(target_os = "windows"))]
+impl InstallerDir {
+    async fn new() -> Result<Self> {
+        Ok(Self(
+            tempfile::Builder::new()
+                .prefix("zed-auto-update")
+                .tempdir()?,
+        ))
+    }
+
+    fn path(&self) -> &Path {
+        self.0.path()
+    }
+}
+
+#[cfg(target_os = "windows")]
+struct InstallerDir(PathBuf);
+
+#[cfg(target_os = "windows")]
+impl InstallerDir {
+    async fn new() -> Result<Self> {
+        let installer_dir = std::env::current_exe()?
+            .parent()
+            .context("No parent dir for Zed.exe")?
+            .join("updates");
+        if smol::fs::metadata(&installer_dir).await.is_ok() {
+            smol::fs::remove_dir_all(&installer_dir).await?;
+        }
+        smol::fs::create_dir(&installer_dir).await?;
+        Ok(Self(installer_dir))
+    }
+
+    fn path(&self) -> &Path {
+        self.0.as_path()
+    }
+}
+
 impl AutoUpdater {
     pub fn get(cx: &mut App) -> Option<Entity<Self>> {
         cx.default_global::<GlobalAutoUpdate>().0.clone()
@@ -469,22 +508,21 @@ impl AutoUpdater {
             cx.notify();
         })?;
 
-        let temp_dir = tempfile::Builder::new()
-            .prefix("zed-auto-update")
-            .tempdir()?;
-
+        let installer_dir = InstallerDir::new().await?;
         let filename = match OS {
             "macos" => Ok("Zed.dmg"),
             "linux" => Ok("zed.tar.gz"),
+            "windows" => Ok("ZedUpdateInstaller.exe"),
             _ => Err(anyhow!("not supported: {:?}", OS)),
         }?;
 
+        #[cfg(not(target_os = "windows"))]
         anyhow::ensure!(
-            which("rsync").is_ok(),
+            which::which("rsync").is_ok(),
             "Aborting. Could not find rsync which is required for auto-updates."
         );
 
-        let downloaded_asset = temp_dir.path().join(filename);
+        let downloaded_asset = installer_dir.path().join(filename);
         download_release(&downloaded_asset, release, client, &cx).await?;
 
         this.update(&mut cx, |this, cx| {
@@ -493,8 +531,9 @@ impl AutoUpdater {
         })?;
 
         let binary_path = match OS {
-            "macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
-            "linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
+            "macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
+            "linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
+            "windows" => install_release_windows(downloaded_asset).await,
             _ => Err(anyhow!("not supported: {:?}", OS)),
         }?;
 
@@ -629,7 +668,7 @@ async fn download_release(
 }
 
 async fn install_release_linux(
-    temp_dir: &tempfile::TempDir,
+    temp_dir: &InstallerDir,
     downloaded_tar_gz: PathBuf,
     cx: &AsyncApp,
 ) -> Result<PathBuf> {
@@ -696,7 +735,7 @@ async fn install_release_linux(
 }
 
 async fn install_release_macos(
-    temp_dir: &tempfile::TempDir,
+    temp_dir: &InstallerDir,
     downloaded_dmg: PathBuf,
     cx: &AsyncApp,
 ) -> Result<PathBuf> {
@@ -743,3 +782,41 @@ async fn install_release_macos(
 
     Ok(running_app_path)
 }
+
+async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
+    let output = Command::new(downloaded_installer)
+        .arg("/verysilent")
+        .arg("/update=true")
+        .arg("!desktopicon")
+        .arg("!quicklaunchicon")
+        .output()
+        .await?;
+    anyhow::ensure!(
+        output.status.success(),
+        "failed to start installer: {:?}",
+        String::from_utf8_lossy(&output.stderr)
+    );
+    Ok(std::env::current_exe()?)
+}
+
+pub fn check_pending_installation() -> bool {
+    let Some(installer_path) = std::env::current_exe()
+        .ok()
+        .and_then(|p| p.parent().map(|p| p.join("updates")))
+    else {
+        return false;
+    };
+
+    // The installer will create a flag file after it finishes updating
+    let flag_file = installer_path.join("versions.txt");
+    if flag_file.exists() {
+        if let Some(helper) = installer_path
+            .parent()
+            .map(|p| p.join("tools\\auto_update_helper.exe"))
+        {
+            let _ = std::process::Command::new(helper).spawn();
+            return true;
+        }
+    }
+    false
+}

crates/auto_update_helper/Cargo.toml 🔗

@@ -0,0 +1,29 @@
+[package]
+name = "auto_update_helper"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[[bin]]
+name = "auto_update_helper"
+path = "src/auto_update_helper.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+log.workspace = true
+simplelog.workspace = true
+workspace-hack.workspace = true
+
+[target.'cfg(target_os = "windows")'.dependencies]
+windows.workspace = true
+
+[target.'cfg(target_os = "windows")'.build-dependencies]
+winresource = "0.1"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-pc-windows-msvc"]

crates/auto_update_helper/build.rs 🔗

@@ -0,0 +1,15 @@
+fn main() {
+    #[cfg(target_os = "windows")]
+    {
+        println!("cargo:rerun-if-changed=manifest.xml");
+
+        let mut res = winresource::WindowsResource::new();
+        res.set_manifest_file("manifest.xml");
+        res.set_icon("app-icon.ico");
+
+        if let Err(e) = res.compile() {
+            eprintln!("{}", e);
+            std::process::exit(1);
+        }
+    }
+}

crates/auto_update_helper/manifest.xml 🔗

@@ -0,0 +1,16 @@
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+    <asmv3:application>
+        <asmv3:windowsSettings>
+            <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
+            <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+        </asmv3:windowsSettings>
+    </asmv3:application>
+    <dependency>
+        <dependentAssembly>
+            <assemblyIdentity type='win32'
+                name='Microsoft.Windows.Common-Controls'
+                version='6.0.0.0' processorArchitecture='*'
+                publicKeyToken='6595b64144ccf1df' />
+        </dependentAssembly>
+    </dependency>
+</assembly>

crates/auto_update_helper/src/auto_update_helper.rs 🔗

@@ -0,0 +1,94 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+#[cfg(target_os = "windows")]
+mod dialog;
+#[cfg(target_os = "windows")]
+mod updater;
+
+#[cfg(target_os = "windows")]
+fn main() {
+    if let Err(e) = windows_impl::run() {
+        log::error!("Error: Zed update failed, {:?}", e);
+        windows_impl::show_error(format!("Error: {:?}", e));
+    }
+}
+
+#[cfg(not(target_os = "windows"))]
+fn main() {}
+
+#[cfg(target_os = "windows")]
+mod windows_impl {
+    use std::path::Path;
+
+    use super::dialog::create_dialog_window;
+    use super::updater::perform_update;
+    use anyhow::{Context, Result};
+    use windows::{
+        Win32::{
+            Foundation::{HWND, LPARAM, WPARAM},
+            UI::WindowsAndMessaging::{
+                DispatchMessageW, GetMessageW, MB_ICONERROR, MB_SYSTEMMODAL, MSG, MessageBoxW,
+                PostMessageW, WM_USER,
+            },
+        },
+        core::HSTRING,
+    };
+
+    pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1;
+    pub(crate) const WM_TERMINATE: u32 = WM_USER + 2;
+
+    pub(crate) fn run() -> Result<()> {
+        let helper_dir = std::env::current_exe()?
+            .parent()
+            .context("No parent directory")?
+            .to_path_buf();
+        init_log(&helper_dir)?;
+        let app_dir = helper_dir
+            .parent()
+            .context("No parent directory")?
+            .to_path_buf();
+
+        log::info!("======= Starting Zed update =======");
+        let (tx, rx) = std::sync::mpsc::channel();
+        let hwnd = create_dialog_window(rx)?.0 as isize;
+        std::thread::spawn(move || {
+            let result = perform_update(app_dir.as_path(), Some(hwnd));
+            tx.send(result).ok();
+            unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok();
+        });
+        unsafe {
+            let mut message = MSG::default();
+            while GetMessageW(&mut message, None, 0, 0).as_bool() {
+                DispatchMessageW(&message);
+            }
+        }
+        Ok(())
+    }
+
+    fn init_log(helper_dir: &Path) -> Result<()> {
+        simplelog::WriteLogger::init(
+            simplelog::LevelFilter::Info,
+            simplelog::Config::default(),
+            std::fs::File::options()
+                .append(true)
+                .create(true)
+                .open(helper_dir.join("auto_update_helper.log"))?,
+        )?;
+        Ok(())
+    }
+
+    pub(crate) fn show_error(mut content: String) {
+        if content.len() > 600 {
+            content.truncate(600);
+            content.push_str("...\n");
+        }
+        let _ = unsafe {
+            MessageBoxW(
+                None,
+                &HSTRING::from(content),
+                windows::core::w!("Error: Zed update failed."),
+                MB_ICONERROR | MB_SYSTEMMODAL,
+            )
+        };
+    }
+}

crates/auto_update_helper/src/dialog.rs 🔗

@@ -0,0 +1,236 @@
+use std::{cell::RefCell, sync::mpsc::Receiver};
+
+use anyhow::{Context as _, Result};
+use windows::{
+    Win32::{
+        Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM},
+        Graphics::Gdi::{
+            BeginPaint, CLEARTYPE_QUALITY, CLIP_DEFAULT_PRECIS, CreateFontW, DEFAULT_CHARSET,
+            DeleteObject, EndPaint, FW_NORMAL, LOGFONTW, OUT_TT_ONLY_PRECIS, PAINTSTRUCT,
+            ReleaseDC, SelectObject, TextOutW,
+        },
+        System::LibraryLoader::GetModuleHandleW,
+        UI::{
+            Controls::{PBM_SETRANGE, PBM_SETSTEP, PBM_STEPIT, PROGRESS_CLASS},
+            WindowsAndMessaging::{
+                CREATESTRUCTW, CS_HREDRAW, CS_VREDRAW, CreateWindowExW, DefWindowProcW,
+                GWLP_USERDATA, GetDesktopWindow, GetWindowLongPtrW, GetWindowRect, HICON,
+                IMAGE_ICON, LR_DEFAULTSIZE, LR_SHARED, LoadImageW, PostQuitMessage, RegisterClassW,
+                SPI_GETICONTITLELOGFONT, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SendMessageW,
+                SetWindowLongPtrW, SystemParametersInfoW, WINDOW_EX_STYLE, WM_CLOSE, WM_CREATE,
+                WM_DESTROY, WM_NCCREATE, WM_PAINT, WNDCLASSW, WS_CAPTION, WS_CHILD, WS_EX_TOPMOST,
+                WS_POPUP, WS_VISIBLE,
+            },
+        },
+    },
+    core::HSTRING,
+};
+
+use crate::{
+    updater::JOBS,
+    windows_impl::{WM_JOB_UPDATED, WM_TERMINATE, show_error},
+};
+
+#[repr(C)]
+#[derive(Debug)]
+struct DialogInfo {
+    rx: Receiver<Result<()>>,
+    progress_bar: isize,
+}
+
+pub(crate) fn create_dialog_window(receiver: Receiver<Result<()>>) -> Result<HWND> {
+    unsafe {
+        let class_name = windows::core::w!("Zed-Auto-Updater-Dialog-Class");
+        let module = GetModuleHandleW(None).context("unable to get module handle")?;
+        let handle = LoadImageW(
+            Some(module.into()),
+            windows::core::PCWSTR(1 as _),
+            IMAGE_ICON,
+            0,
+            0,
+            LR_DEFAULTSIZE | LR_SHARED,
+        )
+        .context("unable to load icon file")?;
+        let wc = WNDCLASSW {
+            lpfnWndProc: Some(wnd_proc),
+            lpszClassName: class_name,
+            style: CS_HREDRAW | CS_VREDRAW,
+            hIcon: HICON(handle.0),
+            ..Default::default()
+        };
+        RegisterClassW(&wc);
+        let mut rect = RECT::default();
+        GetWindowRect(GetDesktopWindow(), &mut rect)
+            .context("unable to get desktop window rect")?;
+        let width = 400;
+        let height = 150;
+        let info = Box::new(RefCell::new(DialogInfo {
+            rx: receiver,
+            progress_bar: 0,
+        }));
+
+        let hwnd = CreateWindowExW(
+            WS_EX_TOPMOST,
+            class_name,
+            windows::core::w!("Zed Editor"),
+            WS_VISIBLE | WS_POPUP | WS_CAPTION,
+            rect.right / 2 - width / 2,
+            rect.bottom / 2 - height / 2,
+            width,
+            height,
+            None,
+            None,
+            None,
+            Some(Box::into_raw(info) as _),
+        )
+        .context("unable to create dialog window")?;
+        Ok(hwnd)
+    }
+}
+
+macro_rules! return_if_failed {
+    ($e:expr) => {
+        match $e {
+            Ok(v) => v,
+            Err(e) => {
+                return LRESULT(e.code().0 as _);
+            }
+        }
+    };
+}
+
+macro_rules! make_lparam {
+    ($l:expr, $h:expr) => {
+        LPARAM(($l as u32 | ($h as u32) << 16) as isize)
+    };
+}
+
+unsafe extern "system" fn wnd_proc(
+    hwnd: HWND,
+    msg: u32,
+    wparam: WPARAM,
+    lparam: LPARAM,
+) -> LRESULT {
+    match msg {
+        WM_NCCREATE => unsafe {
+            let create_struct = lparam.0 as *const CREATESTRUCTW;
+            let info = (*create_struct).lpCreateParams as *mut RefCell<DialogInfo>;
+            let info = Box::from_raw(info);
+            SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(info) as _);
+            DefWindowProcW(hwnd, msg, wparam, lparam)
+        },
+        WM_CREATE => unsafe {
+            // Create progress bar
+            let mut rect = RECT::default();
+            return_if_failed!(GetWindowRect(hwnd, &mut rect));
+            let progress_bar = return_if_failed!(CreateWindowExW(
+                WINDOW_EX_STYLE(0),
+                PROGRESS_CLASS,
+                None,
+                WS_CHILD | WS_VISIBLE,
+                20,
+                50,
+                340,
+                35,
+                Some(hwnd),
+                None,
+                None,
+                None,
+            ));
+            SendMessageW(
+                progress_bar,
+                PBM_SETRANGE,
+                None,
+                Some(make_lparam!(0, JOBS.len() * 10)),
+            );
+            SendMessageW(progress_bar, PBM_SETSTEP, Some(WPARAM(10)), None);
+            with_dialog_data(hwnd, |data| {
+                data.borrow_mut().progress_bar = progress_bar.0 as isize
+            });
+            LRESULT(0)
+        },
+        WM_PAINT => unsafe {
+            let mut ps = PAINTSTRUCT::default();
+            let hdc = BeginPaint(hwnd, &mut ps);
+
+            let font_name = get_system_ui_font_name();
+            let font = CreateFontW(
+                24,
+                0,
+                0,
+                0,
+                FW_NORMAL.0 as _,
+                0,
+                0,
+                0,
+                DEFAULT_CHARSET,
+                OUT_TT_ONLY_PRECIS,
+                CLIP_DEFAULT_PRECIS,
+                CLEARTYPE_QUALITY,
+                0,
+                &HSTRING::from(font_name),
+            );
+            let temp = SelectObject(hdc, font.into());
+            let string = HSTRING::from("Zed Editor is updating...");
+            return_if_failed!(TextOutW(hdc, 20, 15, &string).ok());
+            return_if_failed!(DeleteObject(temp).ok());
+
+            return_if_failed!(EndPaint(hwnd, &ps).ok());
+            ReleaseDC(Some(hwnd), hdc);
+
+            LRESULT(0)
+        },
+        WM_JOB_UPDATED => with_dialog_data(hwnd, |data| {
+            let progress_bar = data.borrow().progress_bar;
+            unsafe { SendMessageW(HWND(progress_bar as _), PBM_STEPIT, None, None) }
+        }),
+        WM_TERMINATE => {
+            with_dialog_data(hwnd, |data| {
+                if let Ok(result) = data.borrow_mut().rx.recv() {
+                    if let Err(e) = result {
+                        log::error!("Failed to update Zed: {:?}", e);
+                        show_error(format!("Error: {:?}", e));
+                    }
+                }
+            });
+            unsafe { PostQuitMessage(0) };
+            LRESULT(0)
+        }
+        WM_CLOSE => LRESULT(0), // Prevent user occasionally closing the window
+        WM_DESTROY => {
+            unsafe { PostQuitMessage(0) };
+            LRESULT(0)
+        }
+        _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
+    }
+}
+
+fn with_dialog_data<F, T>(hwnd: HWND, f: F) -> T
+where
+    F: FnOnce(&RefCell<DialogInfo>) -> T,
+{
+    let raw = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut RefCell<DialogInfo> };
+    let data = unsafe { Box::from_raw(raw) };
+    let result = f(data.as_ref());
+    unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(data) as _) };
+    result
+}
+
+fn get_system_ui_font_name() -> String {
+    unsafe {
+        let mut info: LOGFONTW = std::mem::zeroed();
+        if SystemParametersInfoW(
+            SPI_GETICONTITLELOGFONT,
+            std::mem::size_of::<LOGFONTW>() as u32,
+            Some(&mut info as *mut _ as _),
+            SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
+        )
+        .is_ok()
+        {
+            let font_name = String::from_utf16_lossy(&info.lfFaceName);
+            font_name.trim_matches(char::from(0)).to_owned()
+        } else {
+            "MS Shell Dlg".to_owned()
+        }
+    }
+}

crates/auto_update_helper/src/updater.rs 🔗

@@ -0,0 +1,171 @@
+use std::{
+    os::windows::process::CommandExt,
+    path::Path,
+    time::{Duration, Instant},
+};
+
+use anyhow::{Context, Result};
+use windows::Win32::{
+    Foundation::{HWND, LPARAM, WPARAM},
+    System::Threading::CREATE_NEW_PROCESS_GROUP,
+    UI::WindowsAndMessaging::PostMessageW,
+};
+
+use crate::windows_impl::WM_JOB_UPDATED;
+
+type Job = fn(&Path) -> Result<()>;
+
+#[cfg(not(test))]
+pub(crate) const JOBS: [Job; 6] = [
+    // Delete old files
+    |app_dir| {
+        let zed_executable = app_dir.join("Zed.exe");
+        log::info!("Removing old file: {}", zed_executable.display());
+        std::fs::remove_file(&zed_executable).context(format!(
+            "Failed to remove old file {}",
+            zed_executable.display()
+        ))
+    },
+    |app_dir| {
+        let zed_cli = app_dir.join("bin\\zed.exe");
+        log::info!("Removing old file: {}", zed_cli.display());
+        std::fs::remove_file(&zed_cli)
+            .context(format!("Failed to remove old file {}", zed_cli.display()))
+    },
+    // Copy new files
+    |app_dir| {
+        let zed_executable_source = app_dir.join("install\\Zed.exe");
+        let zed_executable_dest = app_dir.join("Zed.exe");
+        log::info!(
+            "Copying new file {} to {}",
+            zed_executable_source.display(),
+            zed_executable_dest.display()
+        );
+        std::fs::copy(&zed_executable_source, &zed_executable_dest)
+            .map(|_| ())
+            .context(format!(
+                "Failed to copy new file {} to {}",
+                zed_executable_source.display(),
+                zed_executable_dest.display()
+            ))
+    },
+    |app_dir| {
+        let zed_cli_source = app_dir.join("install\\bin\\zed.exe");
+        let zed_cli_dest = app_dir.join("bin\\zed.exe");
+        log::info!(
+            "Copying new file {} to {}",
+            zed_cli_source.display(),
+            zed_cli_dest.display()
+        );
+        std::fs::copy(&zed_cli_source, &zed_cli_dest)
+            .map(|_| ())
+            .context(format!(
+                "Failed to copy new file {} to {}",
+                zed_cli_source.display(),
+                zed_cli_dest.display()
+            ))
+    },
+    // Clean up installer folder and updates folder
+    |app_dir| {
+        let updates_folder = app_dir.join("updates");
+        log::info!("Cleaning up: {}", updates_folder.display());
+        std::fs::remove_dir_all(&updates_folder).context(format!(
+            "Failed to remove updates folder {}",
+            updates_folder.display()
+        ))
+    },
+    |app_dir| {
+        let installer_folder = app_dir.join("install");
+        log::info!("Cleaning up: {}", installer_folder.display());
+        std::fs::remove_dir_all(&installer_folder).context(format!(
+            "Failed to remove installer folder {}",
+            installer_folder.display()
+        ))
+    },
+];
+
+#[cfg(test)]
+pub(crate) const JOBS: [Job; 2] = [
+    |_| {
+        std::thread::sleep(Duration::from_millis(1000));
+        if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
+            match config.as_str() {
+                "err" => Err(std::io::Error::new(
+                    std::io::ErrorKind::Other,
+                    "Simulated error",
+                ))
+                .context("Anyhow!"),
+                _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
+            }
+        } else {
+            Ok(())
+        }
+    },
+    |_| {
+        std::thread::sleep(Duration::from_millis(1000));
+        if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
+            match config.as_str() {
+                "err" => Err(std::io::Error::new(
+                    std::io::ErrorKind::Other,
+                    "Simulated error",
+                ))
+                .context("Anyhow!"),
+                _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
+            }
+        } else {
+            Ok(())
+        }
+    },
+];
+
+pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()> {
+    let hwnd = hwnd.map(|ptr| HWND(ptr as _));
+
+    for job in JOBS.iter() {
+        let start = Instant::now();
+        loop {
+            if start.elapsed().as_secs() > 2 {
+                return Err(anyhow::anyhow!("Timed out"));
+            }
+            match (*job)(app_dir) {
+                Ok(_) => {
+                    unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
+                    break;
+                }
+                Err(err) => {
+                    // Check if it's a "not found" error
+                    let io_err = err.downcast_ref::<std::io::Error>().unwrap();
+                    if io_err.kind() == std::io::ErrorKind::NotFound {
+                        log::warn!("File or folder not found.");
+                        unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
+                        break;
+                    }
+
+                    log::error!("Operation failed: {}", err);
+                    std::thread::sleep(Duration::from_millis(50));
+                }
+            }
+        }
+    }
+    let _ = std::process::Command::new(app_dir.join("Zed.exe"))
+        .creation_flags(CREATE_NEW_PROCESS_GROUP.0)
+        .spawn();
+    log::info!("Update completed successfully");
+    Ok(())
+}
+
+#[cfg(test)]
+mod test {
+    use super::perform_update;
+
+    #[test]
+    fn test_perform_update() {
+        let app_dir = std::path::Path::new("C:/");
+        assert!(perform_update(app_dir, None).is_ok());
+
+        // Simulate a timeout
+        unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
+        let ret = perform_update(app_dir, None);
+        assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
+    }
+}

crates/zed/src/main.rs 🔗

@@ -168,6 +168,16 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
 }
 
 fn main() {
+    // Check if there is a pending installer
+    // If there is, run the installer and exit
+    // And we don't want to run the installer if we are not the first instance
+    #[cfg(target_os = "windows")]
+    let is_first_instance = crate::zed::windows_only_instance::is_first_instance();
+    #[cfg(target_os = "windows")]
+    if is_first_instance && auto_update::check_pending_installation() {
+        return;
+    }
+
     let args = Args::parse();
 
     // Set custom data directory.
@@ -236,27 +246,30 @@ fn main() {
 
     let (open_listener, mut open_rx) = OpenListener::new();
 
-    let failed_single_instance_check = if *db::ZED_STATELESS
-        || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
-    {
-        false
-    } else {
-        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
-        {
-            crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
-        }
+    let failed_single_instance_check =
+        if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+            false
+        } else {
+            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+            {
+                crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
+            }
 
-        #[cfg(target_os = "windows")]
-        {
-            !crate::zed::windows_only_instance::check_single_instance(open_listener.clone(), &args)
-        }
+            #[cfg(target_os = "windows")]
+            {
+                !crate::zed::windows_only_instance::handle_single_instance(
+                    open_listener.clone(),
+                    &args,
+                    is_first_instance,
+                )
+            }
 
-        #[cfg(target_os = "macos")]
-        {
-            use zed::mac_only_instance::*;
-            ensure_only_instance() != IsOnlyInstance::Yes
-        }
-    };
+            #[cfg(target_os = "macos")]
+            {
+                use zed::mac_only_instance::*;
+                ensure_only_instance() != IsOnlyInstance::Yes
+            }
+        };
     if failed_single_instance_check {
         println!("zed is already running");
         return;

crates/zed/src/zed/windows_only_instance.rs 🔗

@@ -25,7 +25,7 @@ use windows::{
 
 use crate::{Args, OpenListener};
 
-pub fn check_single_instance(opener: OpenListener, args: &Args) -> bool {
+pub fn is_first_instance() -> bool {
     unsafe {
         CreateMutexW(
             None,
@@ -34,9 +34,11 @@ pub fn check_single_instance(opener: OpenListener, args: &Args) -> bool {
         )
         .expect("Unable to create instance mutex.")
     };
-    let first_instance = unsafe { GetLastError() } != ERROR_ALREADY_EXISTS;
+    unsafe { GetLastError() != ERROR_ALREADY_EXISTS }
+}
 
-    if first_instance {
+pub fn handle_single_instance(opener: OpenListener, args: &Args, is_first_instance: bool) -> bool {
+    if is_first_instance {
         // We are the first instance, listen for messages sent from other instances
         std::thread::spawn(move || with_pipe(|url| opener.open_urls(vec![url])));
     } else if !args.foreground {
@@ -44,7 +46,7 @@ pub fn check_single_instance(opener: OpenListener, args: &Args) -> bool {
         send_args_to_instance(args).log_err();
     }
 
-    first_instance
+    is_first_instance
 }
 
 fn with_pipe(f: impl Fn(String)) {

tooling/workspace-hack/Cargo.toml 🔗

@@ -512,6 +512,8 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["ring"]
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 winapi = { version = "0.3", default-features = false, features = ["cfg", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "in6addr", "inaddr", "knownfolders", "minwinbase", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "sysinfoapi", "winbase", "windef", "winerror", "winioctl"] }
+windows-core = { version = "0.61" }
+windows-numerics = { version = "0.2" }
 windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
 windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
 windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] }
@@ -533,6 +535,8 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["ring"]
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 winapi = { version = "0.3", default-features = false, features = ["cfg", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "in6addr", "inaddr", "knownfolders", "minwinbase", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "sysinfoapi", "winbase", "windef", "winerror", "winioctl"] }
+windows-core = { version = "0.61" }
+windows-numerics = { version = "0.2" }
 windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
 windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
 windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] }