Fix macOS relaunch activation (#583)

This commit is contained in:
Gregory Schier
2026-08-18 09:56:40 -07:00
committed by GitHub
parent 131b7e5ab1
commit 3f202ff664
3 changed files with 112 additions and 2 deletions
+3 -1
View File
@@ -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<R: Runtime>(
}
async fn cmd_restart<R: Runtime>(app_handle: AppHandle<R>) -> YaakResult<()> {
app_handle.request_restart();
restart::request_restart(&app_handle);
Ok(())
}
@@ -1408,6 +1409,7 @@ pub fn run() {
}
});
}
RunEvent::Exit => restart::relaunch_if_requested(),
_ => {}
};
});
+107
View File
@@ -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<R: Runtime>(app_handle: &AppHandle<R>) {
#[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<PathBuf> {
app_bundle_from_executable(&std::env::current_exe().ok()?)
}
#[cfg(any(target_os = "macos", test))]
fn app_bundle_from_executable(executable: &Path) -> Option<PathBuf> {
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
);
}
}
+2 -1
View File
@@ -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<R: Runtime>(window: &WebviewWindow<R>, update: &Upd
))
.blocking_show()
{
window.app_handle().request_restart();
restart::request_restart(window.app_handle());
}
}
Err(e) => {