From a6571e71e47fbb74262eb5a3596afc14d02a8d13 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:30:23 +0800 Subject: [PATCH] feat: macos, update dmg (#13539) * feat: macos, update dmg Signed-off-by: fufesou * Update src/platform/macos.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: macos update, remove temp update dir Signed-off-by: fufesou * refact: macos update, print Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/common.rs | 4 +++ src/core_main.rs | 35 +++++++++++++++++++----- src/platform/macos.rs | 62 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/src/common.rs b/src/common.rs index 4ac3b6cd9..2dbc4c964 100644 --- a/src/common.rs +++ b/src/common.rs @@ -115,6 +115,10 @@ pub fn global_init() -> bool { crate::server::wayland::init(); } } + #[cfg(target_os = "macos")] + { + crate::platform::macos::try_remove_temp_update_dir(None); + } true } diff --git a/src/core_main.rs b/src/core_main.rs index ecef5a45a..ab301e3d4 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -300,14 +300,35 @@ pub fn core_main() -> Option> { { use crate::platform; if args[0] == "--update" { - let _text = match platform::update_me() { - Ok(_) => { - log::info!("{}", translate("Update successfully!".to_string())); + if args.len() > 1 && args[1].ends_with(".dmg") { + // Version check is unnecessary unless downgrading to an older version + // that lacks "update dmg" support. This is a special case since we cannot + // detect the version before extracting the DMG, so we skip the check. + let dmg_path = &args[1]; + println!("Updating from DMG: {}", dmg_path); + match platform::update_from_dmg(dmg_path) { + Ok(_) => { + println!("Update process from DMG started successfully."); + // The new process will handle the rest. We can exit. + } + Err(err) => { + eprintln!("Failed to start update from DMG: {}", err); + } } - Err(err) => { - log::error!("Update failed with error: {err}"); - } - }; + } else { + println!("Starting update process..."); + log::info!("Starting update process..."); + let _text = match platform::update_me() { + Ok(_) => { + println!("{}", translate("Update successfully!".to_string())); + log::info!("Update successfully!"); + } + Err(err) => { + eprintln!("Update failed with error: {}", err); + log::error!("Update failed with error: {err}"); + } + }; + } return None; } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 4bf419952..bc13260a5 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -38,6 +38,8 @@ static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); static mut LATEST_SEED: i32 = 0; +// Using a fixed temporary directory for updates is preferable to +// using one that includes the custom client name. const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; extern "C" { @@ -714,6 +716,14 @@ pub fn quit_gui() { }; } +#[inline] +pub fn try_remove_temp_update_dir(dir: Option<&str>) { + let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR)); + if target_path.exists() { + std::fs::remove_dir_all(target_path).ok(); + } +} + pub fn update_me() -> ResultType<()> { let is_installed_daemon = is_installed_daemon(false); let option_stop_service = "stop-service"; @@ -733,6 +743,7 @@ pub fn update_me() -> ResultType<()> { bail!("Unknown app directory of current exe file: {:?}", cmd); }; + let app_name = crate::get_app_name(); if is_installed_daemon && !is_service_stopped { let agent = format!("{}_server.plist", crate::get_full_name()); let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); @@ -749,12 +760,13 @@ pub fn update_me() -> ResultType<()> { let update_body = format!( r#" do shell script " -pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app -" with prompt "RustDesk wants to update itself" with administrator privileges +pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{app_name}.app' && ditto '{app_dir}' '/Applications/{app_name}.app' && chown -R {user}:staff '/Applications/{app_name}.app' && xattr -r -d com.apple.quarantine '/Applications/{app_name}.app' +" with prompt "{app_name} wants to update itself" with administrator privileges "#, - std::process::id(), - app_dir, - get_active_username() + app_name = app_name, + pid = std::process::id(), + app_dir = app_dir, + user = get_active_username() ); match Command::new("osascript") .arg("-e") @@ -772,7 +784,7 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes } std::process::Command::new("open") .arg("-n") - .arg(&format!("/Applications/{}.app", crate::get_app_name())) + .arg(&format!("/Applications/{}.app", app_name)) .spawn() .ok(); // leave open a little time @@ -780,6 +792,15 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes Ok(()) } +pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { + println!("Starting update from DMG: {}", dmg_path); + extract_dmg(dmg_path, UPDATE_TEMP_DIR)?; + println!("DMG extracted"); + update_extracted(UPDATE_TEMP_DIR)?; + println!("Update process started"); + Ok(()) +} + pub fn update_to(_file: &str) -> ResultType<()> { update_extracted(UPDATE_TEMP_DIR)?; Ok(()) @@ -811,10 +832,14 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } std::fs::create_dir_all(target_path)?; - Command::new("hdiutil") + let status = Command::new("hdiutil") .args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path]) .status()?; + if !status.success() { + bail!("Failed to attach DMG image at {}: {:?}", dmg_path, status); + } + struct DmgGuard(&'static str); impl Drop for DmgGuard { fn drop(&mut self) { @@ -825,7 +850,7 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } let _guard = DmgGuard(mount_point); - let app_name = "RustDesk.app"; + let app_name = format!("{}.app", crate::get_app_name()); let src_path = format!("{}/{}", mount_point, app_name); let dest_path = format!("{}/{}", target_dir, app_name); @@ -834,7 +859,12 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { .status()?; if !copy_status.success() { - bail!("Failed to copy application {:?}", copy_status); + bail!( + "Failed to copy application from {} to {}: {:?}", + src_path, + dest_path, + copy_status + ); } if !Path::new(&dest_path).exists() { @@ -848,9 +878,13 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } fn update_extracted(target_dir: &str) -> ResultType<()> { - let exe_path = format!("{}/RustDesk.app/Contents/MacOS/RustDesk", target_dir); + let app_name = crate::get_app_name(); + let exe_path = format!( + "{}/{}.app/Contents/MacOS/{}", + target_dir, app_name, app_name + ); let _child = unsafe { - Command::new(&exe_path) + if let Err(e) = Command::new(&exe_path) .arg("--update") .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -859,7 +893,11 @@ fn update_extracted(target_dir: &str) -> ResultType<()> { hbb_common::libc::setsid(); Ok(()) }) - .spawn()? + .spawn() + { + try_remove_temp_update_dir(Some(target_dir)); + bail!(e); + } }; Ok(()) }