Try to fix auto-updates when Explorer.exe holds Zed.exe (#50332)

Conrad Irwin and Jakub Konka created

Release Notes:

- Windows: make auto-update more robust in the face of apps holding the
Zed.exe handle

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>

Change summary

Cargo.lock                               |   1 
Cargo.toml                               |   1 
crates/auto_update_helper/Cargo.toml     |   1 
crates/auto_update_helper/src/updater.rs | 112 +++++++++++++++++++++++++
4 files changed, 112 insertions(+), 3 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1352,6 +1352,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "log",
+ "scopeguard",
  "simplelog",
  "tempfile",
  "windows 0.61.3",

Cargo.toml 🔗

@@ -815,6 +815,7 @@ features = [
     "Win32_System_Ole",
     "Win32_System_Performance",
     "Win32_System_Pipes",
+    "Win32_System_RestartManager",
     "Win32_System_SystemInformation",
     "Win32_System_SystemServices",
     "Win32_System_Threading",

crates/auto_update_helper/Cargo.toml 🔗

@@ -19,6 +19,7 @@ log.workspace = true
 simplelog.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]
+scopeguard = "1.2"
 windows.workspace = true
 
 [target.'cfg(target_os = "windows")'.dev-dependencies]

crates/auto_update_helper/src/updater.rs 🔗

@@ -1,13 +1,22 @@
 use std::{
+    ffi::OsStr,
+    os::windows::ffi::OsStrExt,
     path::Path,
     sync::LazyLock,
     time::{Duration, Instant},
 };
 
 use anyhow::{Context as _, Result};
-use windows::Win32::{
-    Foundation::{HWND, LPARAM, WPARAM},
-    UI::WindowsAndMessaging::PostMessageW,
+use windows::{
+    Win32::{
+        Foundation::{HWND, LPARAM, WPARAM},
+        System::RestartManager::{
+            CCH_RM_SESSION_KEY, RmEndSession, RmGetList, RmRegisterResources, RmShutdown,
+            RmStartSession,
+        },
+        UI::WindowsAndMessaging::PostMessageW,
+    },
+    core::{PCWSTR, PWSTR},
 };
 
 use crate::windows_impl::WM_JOB_UPDATED;
@@ -262,9 +271,106 @@ pub(crate) static JOBS: LazyLock<[Job; 9]> = LazyLock::new(|| {
     ]
 });
 
+/// Attempts to use Windows Restart Manager to release file handles held by other processes
+/// (e.g., Explorer.exe) on the files we need to move during the update.
+///
+/// This is a best-effort operation - if it fails, we'll still try the update and rely on
+/// the retry logic.
+fn release_file_handles(app_dir: &Path) -> Result<()> {
+    // Files that commonly get locked by Explorer or other processes
+    let files_to_release = [
+        app_dir.join("Zed.exe"),
+        app_dir.join("bin\\Zed.exe"),
+        app_dir.join("bin\\zed"),
+        app_dir.join("conpty.dll"),
+    ];
+
+    log::info!("Attempting to release file handles using Restart Manager...");
+
+    let mut session: u32 = 0;
+    let mut session_key = [0u16; CCH_RM_SESSION_KEY as usize + 1];
+
+    // Start a Restart Manager session
+    let err = unsafe {
+        RmStartSession(
+            &mut session,
+            Some(0),
+            PWSTR::from_raw(session_key.as_mut_ptr()),
+        )
+    };
+    if err.is_err() {
+        anyhow::bail!("RmStartSession failed: {err:?}");
+    }
+
+    // Ensure we end the session when done
+    let _session_guard = scopeguard::guard(session, |s| {
+        let _ = unsafe { RmEndSession(s) };
+    });
+
+    // Convert paths to wide strings for Windows API
+    let wide_paths: Vec<Vec<u16>> = files_to_release
+        .iter()
+        .filter(|p| p.exists())
+        .map(|p| {
+            OsStr::new(p)
+                .encode_wide()
+                .chain(std::iter::once(0))
+                .collect()
+        })
+        .collect();
+
+    if wide_paths.is_empty() {
+        log::info!("No files to release handles for");
+        return Ok(());
+    }
+
+    let pcwstr_paths: Vec<PCWSTR> = wide_paths
+        .iter()
+        .map(|p| PCWSTR::from_raw(p.as_ptr()))
+        .collect();
+
+    // Register the files we want to modify
+    let err = unsafe { RmRegisterResources(session, Some(&pcwstr_paths), None, None) };
+    if err.is_err() {
+        anyhow::bail!("RmRegisterResources failed: {err:?}");
+    }
+
+    // Check if any processes are using these files
+    let mut needed: u32 = 0;
+    let mut count: u32 = 0;
+    let mut reboot_reasons: u32 = 0;
+    let _ = unsafe { RmGetList(session, &mut needed, &mut count, None, &mut reboot_reasons) };
+
+    if needed == 0 {
+        log::info!("No processes are holding handles to the files");
+        return Ok(());
+    }
+
+    log::info!(
+        "{} process(es) are holding handles to the files, requesting release...",
+        needed
+    );
+
+    // Request processes to release their handles
+    // RmShutdown with flags=0 asks applications to release handles gracefully
+    // For Explorer, this typically releases icon cache handles without closing Explorer
+    let err = unsafe { RmShutdown(session, 0, None) };
+    if err.is_err() {
+        anyhow::bail!("RmShutdown failed: {:?}", err);
+    }
+
+    log::info!("Successfully requested handle release");
+    Ok(())
+}
+
 pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
     let hwnd = hwnd.map(|ptr| HWND(ptr as _));
 
+    // Try to release file handles before starting the update
+    if let Err(e) = release_file_handles(app_dir) {
+        log::warn!("Restart Manager failed (will continue anyway): {}", e);
+    }
+
     let mut last_successful_job = None;
     'outer: for (i, job) in JOBS.iter().enumerate() {
         let start = Instant::now();