Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/restart-may-not-publish-exit-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch:bug
---

`AppHandle::restart()` now waits for `RunEvent::Exit` to be delivered before restarting the application.
92 changes: 79 additions & 13 deletions crates/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ use tauri_utils::{assets::AssetsIter, PackageInfo};

use std::{
borrow::Cow,
cell::UnsafeCell,
collections::HashMap,
fmt,
sync::{mpsc::Sender, Arc, MutexGuard},
sync::{mpsc::Sender, Arc, Mutex, MutexGuard},
thread::ThreadId,
};

use crate::{event::EventId, runtime::RuntimeHandle, Event, EventTarget};
Expand Down Expand Up @@ -73,14 +75,19 @@ pub const RESTART_EXIT_CODE: i32 = i32::MAX;

/// Api exposed on the `ExitRequested` event.
#[derive(Debug)]
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();
}
}
}

Expand Down Expand Up @@ -339,7 +346,19 @@ 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<Option<EventLoop<R>>>>,
}

type EventCallback<R> = Box<dyn FnMut(&AppHandle<R>, RunEvent) + 'static>;

#[derive(Debug)]
struct EventLoop<R: Runtime> {
main_thread_id: ThreadId,
callback: Arc<UnsafeCell<EventCallback<R>>>,
}
// we ensure the EventLoop is only referenced on the main thread
unsafe impl<R: Runtime> Send for EventLoop<R> {}
unsafe impl<R: Runtime> Sync for EventLoop<R> {}

/// APIs specific to the wry runtime.
#[cfg(feature = "wry")]
Expand Down Expand Up @@ -375,6 +394,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(),
}
}
}
Expand Down Expand Up @@ -469,10 +489,36 @@ 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`] and [`RunEvent::Exit`].
pub fn restart(&self) -> ! {
if self.runtime_handle.request_exit(RESTART_EXIT_CODE).is_err() {
self.cleanup_before_exit();
let already_exited = self.manager.event_loop_exited();
if already_exited {
log::debug!("restart already called, exiting early");
crate::process::restart(&self.env());
}

let event_loop_lock = self.event_loop.lock().unwrap();
if let Some(event_loop) = &*event_loop_lock {
if event_loop.main_thread_id == std::thread::current().id() {
log::debug!("restart triggered on the main thread");
self.manager.notify_event_loop_exit();
// we're running on the main thread, so we must trigger the Exit event manually
(unsafe { &mut *event_loop.callback.get() })(self, RunEvent::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();
}
}
}
} else {
log::debug!("restart triggered while event loop is not running");
}
crate::process::restart(&self.env());
}
Expand Down Expand Up @@ -1069,25 +1115,35 @@ impl<R: Runtime> App<R> {
/// _ => {}
/// });
/// ```
pub fn run<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(mut self, mut callback: F) {
pub fn run<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(mut self, callback: F) {
let app_handle = self.handle().clone();
let manager = self.manager.clone();
let callback = Arc::new(UnsafeCell::new(
Box::new(callback) as Box<dyn FnMut(&AppHandle<R>, RunEvent) + 'static>
));

app_handle.event_loop.lock().unwrap().replace(EventLoop {
main_thread_id: std::thread::current().id(),
callback: callback.clone(),
});

self.runtime.take().unwrap().run(move |event| match event {
RuntimeRunEvent::Ready => {
if let Err(e) = setup(&mut self) {
panic!("Failed to setup app: {e}");
}
let event = on_event_loop_event(&app_handle, RuntimeRunEvent::Ready, &manager);
callback(&app_handle, event);
(unsafe { &mut *callback.get() })(&app_handle, event);
}
RuntimeRunEvent::Exit => {
let event = on_event_loop_event(&app_handle, RuntimeRunEvent::Exit, &manager);
callback(&app_handle, event);
(unsafe { &mut *callback.get() })(&app_handle, event);
app_handle.cleanup_before_exit();
manager.notify_event_loop_exit();
}
_ => {
let event = on_event_loop_event(&app_handle, event, &manager);
callback(&app_handle, event);
(unsafe { &mut *callback.get() })(&app_handle, event);
}
});
}
Expand Down Expand Up @@ -1115,7 +1171,7 @@ impl<R: Runtime> App<R> {
/// }
/// ```
#[cfg(desktop)]
pub fn run_iteration<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(&mut self, mut callback: F) {
pub fn run_iteration<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(&mut self, callback: F) {
let manager = self.manager.clone();
let app_handle = self.handle().clone();

Expand All @@ -1125,9 +1181,18 @@ impl<R: Runtime> App<R> {
}
}

let callback = Arc::new(UnsafeCell::new(
Box::new(callback) as Box<dyn FnMut(&AppHandle<R>, RunEvent) + 'static>
));

app_handle.event_loop.lock().unwrap().replace(EventLoop {
main_thread_id: std::thread::current().id(),
callback: callback.clone(),
});

self.runtime.as_mut().unwrap().run_iteration(move |event| {
let event = on_event_loop_event(&app_handle, event, &manager);
callback(&app_handle, event);
(unsafe { &mut *callback.get() })(&app_handle, event);
})
}
}
Expand Down Expand Up @@ -1963,6 +2028,7 @@ tauri::Builder::default()
handle: AppHandle {
runtime_handle,
manager,
event_loop: Default::default(),
},
ran_setup: false,
};
Expand Down Expand Up @@ -2154,7 +2220,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,
Expand Down
64 changes: 64 additions & 0 deletions crates/tauri/src/manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not super familiar with condvar, is there an advantage in using this over e.g. a multi producer multi consumer channel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, mcmc channel can only one thread at once but condvar can wake multiple threads at once so I used 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> {
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -702,6 +713,59 @@ impl<R: Runtime> AppManager<R> {
pub(crate) fn invoke_key(&self) -> &str {
&self.invoke_key
}

pub(crate) fn event_loop_exited(&self) -> bool {
*self.event_loop_exit_mutex.lock().unwrap()
}

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)]
Expand Down
Loading