diff --git a/Cargo.lock b/Cargo.lock index 96caec077edd4bdf8c02a3e1ff1fc10340d2b9b0..dcecec352bf1426fb76956f04224c66b04143627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,6 +1352,7 @@ version = "0.1.0" dependencies = [ "anyhow", "log", + "scopeguard", "simplelog", "tempfile", "windows 0.61.3", diff --git a/Cargo.toml b/Cargo.toml index 8e1312f032e19b2c2c189677f144f04dd7f4589c..35180020a8d70d83c113172051d12a85f33c55ca 100644 --- a/Cargo.toml +++ b/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", diff --git a/crates/auto_update_helper/Cargo.toml b/crates/auto_update_helper/Cargo.toml index 73c38d80dd12e9c42daa42b7e6f2c9d6975cf47b..aa5bf6ac40b0e1ab20cbde510be5d7f389c7ade8 100644 --- a/crates/auto_update_helper/Cargo.toml +++ b/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] diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 70d5e97c67169ce9737c274f90bc72cbe7ceedf5..7821c908c40873637c4ac3993c320416e2a4b978 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/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> = 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 = 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, 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();