fix: ensure RunEvent::Exit is triggered on restart (#12313)

* fix: AppHandle::restart() may not send RunEvent::Exit event

* docs: add changelog: `AppHandle::restart()` may not send `RunEvent::Exit` event before exiting the application.

* style: cargo fmt

* avoid deadlock on main thread

* do not let the restart be prevented

leads to a deadlock currently

* Apply suggestions from code review

* lint

* do not emit RunEvent on main thread

* re-export RESTART_EXIT_CODE

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
anatawa12
2025-03-16 03:49:42 +09:00
committed by GitHub
parent 51bcafe323
commit b05f82d35b
4 changed files with 111 additions and 7 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:bug
---
`AppHandle::restart()` now waits for `RunEvent::Exit` to be delivered before restarting the application.

View File

@@ -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<ExitRequestedEventAction>);
pub struct ExitRequestApi {
tx: Sender<ExitRequestedEventAction>,
code: Option<i32>,
}
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<R: Runtime> AssetResolver<R> {
pub struct AppHandle<R: Runtime> {
pub(crate) runtime_handle: R::Handle,
pub(crate) manager: Arc<AppManager<R>>,
event_loop: Arc<Mutex<EventLoop>>,
}
#[derive(Debug)]
struct EventLoop {
main_thread_id: ThreadId,
}
/// APIs specific to the wry runtime.
@@ -428,6 +440,7 @@ impl<R: Runtime> Clone for AppHandle<R> {
Self {
runtime_handle: self.runtime_handle.clone(),
manager: self.manager.clone(),
event_loop: self.event_loop.clone(),
}
}
}
@@ -522,10 +535,27 @@ impl<R: Runtime> AppHandle<R> {
}
}
/// 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<R: Runtime> App<R> {
pub fn run<F: FnMut(&AppHandle<R>, 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<R: Runtime> App<R> {
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<R: Runtime> App<R> {
}
}
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<R: Runtime>(
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,

View File

@@ -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::{

View File

@@ -221,6 +221,13 @@ pub struct AppManager<R: Runtime> {
pub(crate) invoke_key: String,
pub(crate) channel_interceptor: Option<ChannelInterceptor<R>>,
// the value is set to true when the event loop is already exited
event_loop_exit_mutex: Mutex<bool>,
event_loop_exit_condvar: std::sync::Condvar,
// number of threads that request to NOT exit process
impede_exit_count: Mutex<usize>,
impede_exit_condvar: std::sync::Condvar,
}
impl<R: Runtime> fmt::Debug for AppManager<R> {
@@ -320,6 +327,10 @@ impl<R: Runtime> AppManager<R> {
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<R: Runtime> AppManager<R> {
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<Self>) -> ImpedeScope<R> {
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<Self>) -> ImpedeScope<R> {
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<R: Runtime> {
app_manager: Arc<AppManager<R>>,
}
impl<R: Runtime> Drop for ImpedeScope<R> {
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)]