logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
[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
[target.'cfg(target_os = "windows")'.dependencies]
scopeguard = "1.2"
windows.workspace = true
[target.'cfg(target_os = "windows")'.dev-dependencies]
tempfile.workspace = true
[build-dependencies]
windows_resources = { path = "../windows_resources" }
[package.metadata.docs.rs]
targets = ["x86_64-pc-windows-msvc"]

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

View File

@@ -0,0 +1,9 @@
fn main() {
#[cfg(target_os = "windows")]
{
println!("cargo:rerun-if-env-changed=RELEASE_CHANNEL");
println!("cargo:rerun-if-env-changed=GITHUB_RUN_NUMBER");
windows_resources::compile(true).expect("failed to compile Windows resources");
}
}

View File

@@ -0,0 +1,147 @@
#![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::{borrow::Cow, path::Path};
use super::dialog::create_dialog_window;
use super::updater::perform_update;
use anyhow::{Context as _, 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;
#[derive(Debug, Default)]
struct Args {
launch: bool,
}
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;
let args = parse_args(std::env::args().skip(1));
std::thread::spawn(move || {
let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch);
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(())
}
fn parse_args(input: impl IntoIterator<Item = String>) -> Args {
let mut args: Args = Args { launch: true };
let mut input = input.into_iter();
if let Some(arg) = input.next() {
let launch_arg;
if arg == "--launch" {
launch_arg = input.next().map(Cow::Owned);
} else if let Some(rest) = arg.strip_prefix("--launch=") {
launch_arg = Some(Cow::Borrowed(rest));
} else {
launch_arg = None;
}
if launch_arg.as_deref() == Some("false") {
args.launch = false;
}
}
args
}
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,
)
};
}
#[cfg(test)]
mod tests {
use crate::windows_impl::parse_args;
#[test]
fn test_parse_args() {
// launch can be specified via two separate arguments
assert!(parse_args(["--launch".into(), "true".into()]).launch);
assert!(!parse_args(["--launch".into(), "false".into()]).launch);
// launch can be specified via one single argument
assert!(parse_args(["--launch=true".into()]).launch);
assert!(!parse_args(["--launch=false".into()]).launch);
// launch defaults to true on no arguments
assert!(parse_args([]).launch);
// launch defaults to true on invalid arguments
assert!(parse_args(["--launch".into()]).launch);
assert!(parse_args(["--launch=".into()]).launch);
assert!(parse_args(["--launch=invalid".into()]).launch);
}
}
}

View File

@@ -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"),
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("Updating Zed...");
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()
&& 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()
}
}
}

View File

@@ -0,0 +1,466 @@
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},
System::RestartManager::{
CCH_RM_SESSION_KEY, RmEndSession, RmGetList, RmRegisterResources, RmShutdown,
RmStartSession,
},
UI::WindowsAndMessaging::PostMessageW,
},
core::{PCWSTR, PWSTR},
};
use crate::windows_impl::WM_JOB_UPDATED;
pub(crate) struct Job {
pub apply: Box<dyn Fn(&Path) -> Result<()> + Send + Sync>,
pub rollback: Box<dyn Fn(&Path) -> Result<()> + Send + Sync>,
}
impl Job {
pub fn mkdir(name: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let dir = app_dir.join(name);
std::fs::create_dir_all(&dir)
.context(format!("Failed to create directory {}", dir.display()))
}),
rollback: Box::new(move |app_dir| {
let dir = app_dir.join(name);
std::fs::remove_dir_all(&dir)
.context(format!("Failed to remove directory {}", dir.display()))
}),
}
}
pub fn mkdir_if_exists(name: &'static Path, check: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let dir = app_dir.join(name);
let check = app_dir.join(check);
if check.exists() {
std::fs::create_dir_all(&dir)
.context(format!("Failed to create directory {}", dir.display()))?
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let dir = app_dir.join(name);
if dir.exists() {
std::fs::remove_dir_all(&dir)
.context(format!("Failed to remove directory {}", dir.display()))?
}
Ok(())
}),
}
}
pub fn move_file(filename: &'static Path, new_filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
log::info!(
"Moving file: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&old_file, new_file)
.context(format!("Failed to move file {}", old_file.display()))
}),
rollback: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
log::info!(
"Rolling back file move: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&new_file, &old_file).context(format!(
"Failed to rollback file move {}->{}",
new_file.display(),
old_file.display()
))
}),
}
}
pub fn move_if_exists(filename: &'static Path, new_filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
if old_file.exists() {
log::info!(
"Moving file: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&old_file, new_file)
.context(format!("Failed to move file {}", old_file.display()))?;
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
if new_file.exists() {
log::info!(
"Rolling back file move: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&new_file, &old_file).context(format!(
"Failed to rollback file move {}->{}",
new_file.display(),
old_file.display()
))?
}
Ok(())
}),
}
}
pub fn rmdir_nofail(filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let filename = app_dir.join(filename);
log::info!("Removing file: {}", filename.display());
if let Err(e) = std::fs::remove_dir_all(&filename) {
log::warn!("Failed to remove directory: {}", e);
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let filename = app_dir.join(filename);
anyhow::bail!(
"Delete operations cannot be rolled back, file: {}",
filename.display()
)
}),
}
}
}
#[cfg(not(test))]
pub(crate) static JOBS: LazyLock<[Job; 22]> = LazyLock::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
[
// Move old files
// Not deleting because installing new files can fail
Job::mkdir(p("old")),
Job::move_file(p("Zed.exe"), p("old\\Zed.exe")),
Job::mkdir(p("old\\bin")),
Job::move_file(p("bin\\Zed.exe"), p("old\\bin\\Zed.exe")),
Job::move_file(p("bin\\zed"), p("old\\bin\\zed")),
//
// TODO: remove after a few weeks once everyone is on the new version and this file never exists
Job::move_if_exists(p("OpenConsole.exe"), p("old\\OpenConsole.exe")),
Job::mkdir(p("old\\x64")),
Job::mkdir(p("old\\arm64")),
Job::move_if_exists(p("x64\\OpenConsole.exe"), p("old\\x64\\OpenConsole.exe")),
Job::move_if_exists(
p("arm64\\OpenConsole.exe"),
p("old\\arm64\\OpenConsole.exe"),
),
//
Job::move_file(p("conpty.dll"), p("old\\conpty.dll")),
// Copy new files
Job::move_file(p("install\\Zed.exe"), p("Zed.exe")),
Job::move_file(p("install\\bin\\Zed.exe"), p("bin\\Zed.exe")),
Job::move_file(p("install\\bin\\zed"), p("bin\\zed")),
//
Job::mkdir_if_exists(p("x64"), p("install\\x64")),
Job::mkdir_if_exists(p("arm64"), p("install\\arm64")),
Job::move_if_exists(
p("install\\x64\\OpenConsole.exe"),
p("x64\\OpenConsole.exe"),
),
Job::move_if_exists(
p("install\\arm64\\OpenConsole.exe"),
p("arm64\\OpenConsole.exe"),
),
//
Job::move_file(p("install\\conpty.dll"), p("conpty.dll")),
// Cleanup installer and updates folder
Job::rmdir_nofail(p("updates")),
Job::rmdir_nofail(p("install")),
// Cleanup old installation
Job::rmdir_nofail(p("old")),
]
});
#[cfg(test)]
pub(crate) static JOBS: LazyLock<[Job; 9]> = LazyLock::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
[
Job {
apply: Box::new(|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err1" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
"err2" => Ok(()),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
}),
rollback: Box::new(|_| {
unsafe { std::env::set_var("ZED_AUTO_UPDATE_RB", "rollback1") };
Ok(())
}),
},
Job::mkdir(p("test1")),
Job::mkdir_if_exists(p("test_exists"), p("test1")),
Job::mkdir_if_exists(p("test_missing"), p("dont")),
Job {
apply: Box::new(|folder| {
std::fs::write(folder.join("test1/test"), "test")?;
Ok(())
}),
rollback: Box::new(|folder| {
std::fs::remove_file(folder.join("test1/test"))?;
Ok(())
}),
},
Job::move_file(p("test1/test"), p("test1/moved")),
Job::move_if_exists(p("test1/test"), p("test1/noop")),
Job {
apply: Box::new(|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err1" => Ok(()),
"err2" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
}),
rollback: Box::new(|_| Ok(())),
},
Job::rmdir_nofail(p("test1/nofolder")),
]
});
/// 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();
loop {
if start.elapsed().as_secs() > 2 {
log::error!("Timed out, rolling back");
break 'outer;
}
match (job.apply)(app_dir) {
Ok(_) => {
last_successful_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
Err(err) => match err.downcast_ref::<std::io::Error>() {
Some(io_err) => match io_err.kind() {
std::io::ErrorKind::NotFound => {
log::error!("Operation failed with file not found, aborting: {}", err);
break 'outer;
}
_ => {
log::error!("Operation failed (retrying): {}", err);
std::thread::sleep(Duration::from_millis(50));
}
},
None => {
log::error!("Operation failed with unexpected error, aborting: {}", err);
break 'outer;
}
},
}
}
}
if last_successful_job
.map(|job| job != JOBS.len() - 1)
.unwrap_or(true)
{
let Some(last_successful_job) = last_successful_job else {
anyhow::bail!("Autoupdate failed, nothing to rollback");
};
for job in (0..=last_successful_job).rev() {
let job = &JOBS[job];
if let Err(e) = (job.rollback)(app_dir) {
anyhow::bail!(
"Job rollback failed, the app might be left in an inconsistent state: ({:?})",
e
);
}
}
anyhow::bail!("Autoupdate failed, rollback successful");
}
if launch {
#[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
let _ = std::process::Command::new(app_dir.join("Zed.exe")).spawn();
}
log::info!("Update completed successfully");
Ok(())
}
#[cfg(test)]
mod test {
use super::perform_update;
#[test]
fn test_perform_update() {
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
assert!(perform_update(app_dir, None, false).is_ok());
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
// Simulate a timeout
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err1") };
let ret = perform_update(app_dir, None, false);
assert!(
ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, nothing to rollback")
);
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
// Simulate a timeout
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err2") };
let ret = perform_update(app_dir, None, false);
assert!(
ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, rollback successful")
);
assert!(std::env::var("ZED_AUTO_UPDATE_RB").is_ok_and(|e| e == "rollback1"));
}
}