diff --git a/Cargo.lock b/Cargo.lock index ef35a57f28..911ea64417 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 70634e87bc..ba011f20e1 100644 --- a/Cargo.toml +++ b/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", +] diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 84b4e5d739..1a772710c9 100644 --- a/crates/auto_update/Cargo.toml +++ b/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 diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 77d2037288..390400c048 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/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>>, } -#[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 { + 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 { + 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> { cx.default_global::().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 { @@ -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 { @@ -743,3 +782,41 @@ async fn install_release_macos( Ok(running_app_path) } + +async fn install_release_windows(downloaded_installer: PathBuf) -> Result { + 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 +} diff --git a/crates/auto_update_helper/Cargo.toml b/crates/auto_update_helper/Cargo.toml new file mode 100644 index 0000000000..6581de48d2 --- /dev/null +++ b/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"] diff --git a/crates/auto_update_helper/LICENSE-GPL b/crates/auto_update_helper/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/auto_update_helper/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/auto_update_helper/app-icon.ico b/crates/auto_update_helper/app-icon.ico new file mode 100644 index 0000000000..321e90fcfa Binary files /dev/null and b/crates/auto_update_helper/app-icon.ico differ diff --git a/crates/auto_update_helper/build.rs b/crates/auto_update_helper/build.rs new file mode 100644 index 0000000000..2910632c7f --- /dev/null +++ b/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); + } + } +} diff --git a/crates/auto_update_helper/manifest.xml b/crates/auto_update_helper/manifest.xml new file mode 100644 index 0000000000..5a69b43486 --- /dev/null +++ b/crates/auto_update_helper/manifest.xml @@ -0,0 +1,16 @@ + + + + true + PerMonitorV2 + + + + + + + + diff --git a/crates/auto_update_helper/src/auto_update_helper.rs b/crates/auto_update_helper/src/auto_update_helper.rs new file mode 100644 index 0000000000..b8e4ba26d1 --- /dev/null +++ b/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, + ) + }; + } +} diff --git a/crates/auto_update_helper/src/dialog.rs b/crates/auto_update_helper/src/dialog.rs new file mode 100644 index 0000000000..010ebb4875 --- /dev/null +++ b/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>, + progress_bar: isize, +} + +pub(crate) fn create_dialog_window(receiver: Receiver>) -> Result { + 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; + 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(hwnd: HWND, f: F) -> T +where + F: FnOnce(&RefCell) -> T, +{ + let raw = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut RefCell }; + 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::() 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() + } + } +} diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs new file mode 100644 index 0000000000..1c3fc10655 --- /dev/null +++ b/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) -> 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::().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")); + } +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 98a3ecbd26..21fff01cd5 100644 --- a/crates/zed/src/main.rs +++ b/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; diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 92295b5006..972cad38fe 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/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)) { diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index fac5fec310..c7a2529172 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/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"] }