From 3f202ff664301e86b92bd23d9dd3669059aa481a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 18 Aug 2026 09:56:40 -0700 Subject: [PATCH] Fix macOS relaunch activation (#583) --- crates-tauri/yaak-app-client/src/lib.rs | 4 +- crates-tauri/yaak-app-client/src/restart.rs | 107 ++++++++++++++++++++ crates-tauri/yaak-app-client/src/updates.rs | 3 +- 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 crates-tauri/yaak-app-client/src/restart.rs diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index c12806a3..952b348a 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -68,6 +68,7 @@ mod notifications; mod plugin_events; mod plugins_ext; mod render; +mod restart; mod rpc_ext; mod sync_ext; mod updates; @@ -904,7 +905,7 @@ async fn cmd_grpc_go( } async fn cmd_restart(app_handle: AppHandle) -> YaakResult<()> { - app_handle.request_restart(); + restart::request_restart(&app_handle); Ok(()) } @@ -1408,6 +1409,7 @@ pub fn run() { } }); } + RunEvent::Exit => restart::relaunch_if_requested(), _ => {} }; }); diff --git a/crates-tauri/yaak-app-client/src/restart.rs b/crates-tauri/yaak-app-client/src/restart.rs new file mode 100644 index 00000000..fa984deb --- /dev/null +++ b/crates-tauri/yaak-app-client/src/restart.rs @@ -0,0 +1,107 @@ +#[cfg(target_os = "macos")] +use log::{error, info}; +#[cfg(any(target_os = "macos", test))] +use std::path::{Path, PathBuf}; +#[cfg(target_os = "macos")] +use std::process::{Command, Stdio}; +#[cfg(target_os = "macos")] +use std::sync::atomic::{AtomicBool, Ordering}; +use tauri::{AppHandle, Runtime}; + +#[cfg(target_os = "macos")] +static RELAUNCH_WITH_LAUNCH_SERVICES: AtomicBool = AtomicBool::new(false); + +/// Restart the app without directly spawning the executable on macOS. +/// +/// Tauri's current macOS restart path starts the executable from the dying +/// process. Besides inheriting stale process state, that bypasses +/// LaunchServices and can leave the replacement app running without an active +/// window. Defer the relaunch until `RunEvent::Exit`, when the event loop is +/// already shutting down, and hand it to LaunchServices instead. +pub fn request_restart(app_handle: &AppHandle) { + #[cfg(target_os = "macos")] + if current_app_bundle().is_some() { + info!("Requesting restart through macOS LaunchServices"); + RELAUNCH_WITH_LAUNCH_SERVICES.store(true, Ordering::SeqCst); + app_handle.exit(0); + return; + } + + app_handle.request_restart(); +} + +/// Complete a pending macOS restart after Tauri has emitted its exit events. +pub fn relaunch_if_requested() { + #[cfg(target_os = "macos")] + { + if !RELAUNCH_WITH_LAUNCH_SERVICES.swap(false, Ordering::SeqCst) { + return; + } + + let Some(bundle) = current_app_bundle() else { + error!("Failed to resolve the app bundle for restart"); + return; + }; + + match Command::new("/usr/bin/open") + .arg("-n") + .arg(&bundle) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + { + Ok(_) => info!("Relaunching {} through LaunchServices", bundle.display()), + Err(error) => error!("Failed to relaunch through LaunchServices: {error}"), + } + } +} + +#[cfg(target_os = "macos")] +fn current_app_bundle() -> Option { + app_bundle_from_executable(&std::env::current_exe().ok()?) +} + +#[cfg(any(target_os = "macos", test))] +fn app_bundle_from_executable(executable: &Path) -> Option { + let macos_dir = executable.parent()?; + if macos_dir.file_name()? != "MacOS" { + return None; + } + + let contents_dir = macos_dir.parent()?; + if contents_dir.file_name()? != "Contents" { + return None; + } + + let bundle = contents_dir.parent()?; + if bundle.extension()? != "app" { + return None; + } + + Some(bundle.to_owned()) +} + +#[cfg(test)] +mod tests { + use super::app_bundle_from_executable; + use std::path::{Path, PathBuf}; + + #[test] + fn resolves_macos_app_bundle() { + assert_eq!( + app_bundle_from_executable(Path::new( + "/Applications/Yaak.app/Contents/MacOS/yaak-app-client" + )), + Some(PathBuf::from("/Applications/Yaak.app")) + ); + } + + #[test] + fn ignores_unbundled_executable() { + assert_eq!( + app_bundle_from_executable(Path::new("/workspace/target/debug/yaak-app-client")), + None + ); + } +} diff --git a/crates-tauri/yaak-app-client/src/updates.rs b/crates-tauri/yaak-app-client/src/updates.rs index d55465ba..e5a963fa 100644 --- a/crates-tauri/yaak-app-client/src/updates.rs +++ b/crates-tauri/yaak-app-client/src/updates.rs @@ -4,6 +4,7 @@ use std::time::{Duration, Instant}; use crate::error::Result; use crate::models_ext::QueryManagerExt; +use crate::restart; use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow}; @@ -332,7 +333,7 @@ async fn start_native_update(window: &WebviewWindow, update: &Upd )) .blocking_show() { - window.app_handle().request_restart(); + restart::request_restart(window.app_handle()); } } Err(e) => {