diff --git a/.changes/restart-may-not-publish-exit-event.md b/.changes/restart-may-not-publish-exit-event.md new file mode 100644 index 000000000000..da23ac7fe807 --- /dev/null +++ b/.changes/restart-may-not-publish-exit-event.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:bug +--- + +`AppHandle::restart()` now waits for `RunEvent::Exit` to be delivered before restarting the application. diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index 5de9a294a393..a77d5854a729 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -42,7 +42,8 @@ use std::{ borrow::Cow, collections::HashMap, fmt, - sync::{mpsc::Sender, Arc, MutexGuard}, + sync::{mpsc::Sender, Arc, Mutex, MutexGuard}, + thread::ThreadId, }; use crate::{event::EventId, runtime::RuntimeHandle, Event, EventTarget}; @@ -73,14 +74,19 @@ pub const RESTART_EXIT_CODE: i32 = i32::MAX; /// Api exposed on the `ExitRequested` event. #[derive(Debug, Clone)] -pub struct ExitRequestApi(Sender); +pub struct ExitRequestApi { + tx: Sender, + code: Option, +} impl ExitRequestApi { /// Prevents the app from exiting. /// /// **Note:** This is ignored when using [`AppHandle#method.restart`]. pub fn prevent_exit(&self) { - self.0.send(ExitRequestedEventAction::Prevent).unwrap(); + if self.code != Some(RESTART_EXIT_CODE) { + self.tx.send(ExitRequestedEventAction::Prevent).unwrap(); + } } } @@ -339,6 +345,12 @@ impl AssetResolver { pub struct AppHandle { pub(crate) runtime_handle: R::Handle, pub(crate) manager: Arc>, + event_loop: Arc>, +} + +#[derive(Debug)] +struct EventLoop { + main_thread_id: ThreadId, } /// APIs specific to the wry runtime. @@ -428,6 +440,7 @@ impl Clone for AppHandle { Self { runtime_handle: self.runtime_handle.clone(), manager: self.manager.clone(), + event_loop: self.event_loop.clone(), } } } @@ -522,10 +535,27 @@ impl AppHandle { } } - /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`] and [`RunEvent::Exit`].. + /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`](crate::RESTART_EXIT_CODE) and [`RunEvent::Exit`]. + /// + /// When this function is called on the main thread, we cannot guarantee the delivery of those events, + /// so we skip them and directly restart the process. pub fn restart(&self) -> ! { - if self.runtime_handle.request_exit(RESTART_EXIT_CODE).is_err() { + if self.event_loop.lock().unwrap().main_thread_id == std::thread::current().id() { + log::debug!("restart triggered on the main thread"); self.cleanup_before_exit(); + self.manager.notify_event_loop_exit(); + } else { + log::debug!("restart triggered from a separate thread"); + // we're running on a separate thread, so we must trigger the exit request and wait for it to finish + match self.runtime_handle.request_exit(RESTART_EXIT_CODE) { + Ok(()) => { + let _impede = self.manager.wait_for_event_loop_exit(); + } + Err(e) => { + log::error!("failed to request exit: {e}"); + self.cleanup_before_exit(); + } + } } crate::process::restart(&self.env()); } @@ -1125,6 +1155,9 @@ impl App { pub fn run, RunEvent) + 'static>(mut self, mut callback: F) { let app_handle = self.handle().clone(); let manager = self.manager.clone(); + + app_handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + self.runtime.take().unwrap().run(move |event| match event { RuntimeRunEvent::Ready => { if let Err(e) = setup(&mut self) { @@ -1137,6 +1170,7 @@ impl App { let event = on_event_loop_event(&app_handle, RuntimeRunEvent::Exit, &manager); callback(&app_handle, event); app_handle.cleanup_before_exit(); + manager.notify_event_loop_exit(); } _ => { let event = on_event_loop_event(&app_handle, event, &manager); @@ -1178,6 +1212,8 @@ impl App { } } + app_handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + self.runtime.as_mut().unwrap().run_iteration(move |event| { let event = on_event_loop_event(&app_handle, event, &manager); callback(&app_handle, event); @@ -2016,6 +2052,9 @@ tauri::Builder::default() handle: AppHandle { runtime_handle, manager, + event_loop: Arc::new(Mutex::new(EventLoop { + main_thread_id: std::thread::current().id(), + })), }, ran_setup: false, }; @@ -2207,7 +2246,7 @@ fn on_event_loop_event( RuntimeRunEvent::Exit => RunEvent::Exit, RuntimeRunEvent::ExitRequested { code, tx } => RunEvent::ExitRequested { code, - api: ExitRequestApi(tx), + api: ExitRequestApi { tx, code }, }, RuntimeRunEvent::WindowEvent { label, event } => RunEvent::WindowEvent { label, diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index 30058b6f928e..d1ae7f51c90f 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -214,7 +214,7 @@ use self::manager::EmitPayload; pub use { self::app::{ App, AppHandle, AssetResolver, Builder, CloseRequestApi, ExitRequestApi, RunEvent, - UriSchemeContext, UriSchemeResponder, WebviewEvent, WindowEvent, + UriSchemeContext, UriSchemeResponder, WebviewEvent, WindowEvent, RESTART_EXIT_CODE, }, self::manager::Asset, self::runtime::{ diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs index 4f9369b9965e..3e2eda62aabb 100644 --- a/crates/tauri/src/manager/mod.rs +++ b/crates/tauri/src/manager/mod.rs @@ -221,6 +221,13 @@ pub struct AppManager { pub(crate) invoke_key: String, pub(crate) channel_interceptor: Option>, + + // the value is set to true when the event loop is already exited + event_loop_exit_mutex: Mutex, + event_loop_exit_condvar: std::sync::Condvar, + // number of threads that request to NOT exit process + impede_exit_count: Mutex, + impede_exit_condvar: std::sync::Condvar, } impl fmt::Debug for AppManager { @@ -320,6 +327,10 @@ impl AppManager { pattern: Arc::new(context.pattern), plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts), resources_table: Arc::default(), + event_loop_exit_mutex: Mutex::new(false), + event_loop_exit_condvar: std::sync::Condvar::new(), + impede_exit_count: Mutex::new(0), + impede_exit_condvar: std::sync::Condvar::new(), invoke_key, channel_interceptor, } @@ -693,6 +704,55 @@ impl AppManager { pub(crate) fn invoke_key(&self) -> &str { &self.invoke_key } + + pub(crate) fn notify_event_loop_exit(&self) { + let mut exit = self.event_loop_exit_mutex.lock().unwrap(); + *exit = true; + self.event_loop_exit_condvar.notify_all(); + drop(exit); + + self.wait_impede(); + } + + pub(crate) fn wait_for_event_loop_exit(self: &Arc) -> ImpedeScope { + let impede = self.impede_quit(); + let mut exit = self.event_loop_exit_mutex.lock().unwrap(); + while !*exit { + exit = self.event_loop_exit_condvar.wait(exit).unwrap(); + } + impede + } + + /// When the main loop exits, most runtime would exit the process, so we need to impede the exit + /// if we're going to restart the application. + fn impede_quit(self: &Arc) -> ImpedeScope { + let mut pend_exit_threads = self.impede_exit_count.lock().unwrap(); + *pend_exit_threads += 1; + ImpedeScope { + app_manager: self.clone(), + } + } + + fn wait_impede(&self) { + let mut pend_exit_threads = self.impede_exit_count.lock().unwrap(); + while *pend_exit_threads > 0 { + pend_exit_threads = self.impede_exit_condvar.wait(pend_exit_threads).unwrap(); + } + } +} + +pub struct ImpedeScope { + app_manager: Arc>, +} + +impl Drop for ImpedeScope { + fn drop(&mut self) { + let mut pend_exit_threads = self.app_manager.impede_exit_count.lock().unwrap(); + *pend_exit_threads -= 1; + if *pend_exit_threads == 0 { + self.app_manager.impede_exit_condvar.notify_all(); + } + } } #[cfg(desktop)]