Skip to content

Commit 00e1567

Browse files
authored
refactor(core): exit() and restart() triggers ExitRequested and Exit (#8708)
* refactor(core): exit() and restart() triggers ExitRequested and Exit * update docs * update doc
1 parent 9af90ca commit 00e1567

8 files changed

Lines changed: 75 additions & 18 deletions

File tree

.changes/refactor-exit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch:breaking
3+
---
4+
5+
`AppHandle::exit` and `AppHandle::restart` now go triggers `RunEvent::ExitRequested` and `RunEvent::Exit` and cannot be executed on the event loop handler.

.changes/request-exit.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-runtime": patch:feat
3+
"tauri-runtime-wry": patch:feat
4+
---
5+
6+
Added `RuntimeHandle::request_exit` function.

core/tauri-runtime-wry/src/lib.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,7 @@ pub type CreateWebviewClosure = Box<dyn FnOnce(&Window) -> Result<WebviewWrapper
11851185

11861186
pub enum Message<T: 'static> {
11871187
Task(Box<dyn FnOnce() + Send>),
1188+
RequestExit(i32),
11881189
#[cfg(target_os = "macos")]
11891190
Application(ApplicationMessage),
11901191
Window(WindowId, WindowMessage),
@@ -1969,6 +1970,15 @@ impl<T: UserEvent> RuntimeHandle<T> for WryHandle<T> {
19691970
EventProxy(self.context.proxy.clone())
19701971
}
19711972

1973+
fn request_exit(&self, code: i32) -> Result<()> {
1974+
// NOTE: request_exit cannot use the `send_user_message` function because it accesses the event loop callback
1975+
self
1976+
.context
1977+
.proxy
1978+
.send_event(Message::RequestExit(code))
1979+
.map_err(|_| Error::FailedToSendMessage)
1980+
}
1981+
19721982
// Creates a window by dispatching a message to the event loop.
19731983
// Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock.
19741984
fn create_window<F: Fn(RawWindow) + Send + 'static>(
@@ -2411,6 +2421,7 @@ fn handle_user_message<T: UserEvent>(
24112421
} = context;
24122422
match message {
24132423
Message::Task(task) => task(),
2424+
Message::RequestExit(_code) => panic!("cannot handle RequestExit on the main thread"),
24142425
#[cfg(target_os = "macos")]
24152426
Message::Application(application_message) => match application_message {
24162427
ApplicationMessage::Show => {
@@ -2949,7 +2960,7 @@ fn handle_event_loop<T: UserEvent>(
29492960
let is_empty = windows.borrow().is_empty();
29502961
if is_empty {
29512962
let (tx, rx) = channel();
2952-
callback(RunEvent::ExitRequested { tx });
2963+
callback(RunEvent::ExitRequested { code: None, tx });
29532964

29542965
let recv = rx.try_recv();
29552966
let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent));
@@ -2980,6 +2991,20 @@ fn handle_event_loop<T: UserEvent>(
29802991
}
29812992
}
29822993
Event::UserEvent(message) => match message {
2994+
Message::RequestExit(code) => {
2995+
let (tx, rx) = channel();
2996+
callback(RunEvent::ExitRequested {
2997+
code: Some(code),
2998+
tx,
2999+
});
3000+
3001+
let recv = rx.try_recv();
3002+
let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent));
3003+
3004+
if !should_prevent {
3005+
*control_flow = ControlFlow::Exit;
3006+
}
3007+
}
29833008
Message::Window(id, WindowMessage::Close) => {
29843009
on_window_close(id, windows.clone());
29853010
}

core/tauri-runtime/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ pub enum RunEvent<T: UserEvent> {
153153
Exit,
154154
/// Event loop is about to exit
155155
ExitRequested {
156+
/// The exit code.
157+
code: Option<i32>,
156158
tx: Sender<ExitRequestedEventAction>,
157159
},
158160
/// An event associated with a window.
@@ -204,6 +206,9 @@ pub trait RuntimeHandle<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'st
204206
/// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop.
205207
fn create_proxy(&self) -> <Self::Runtime as Runtime<T>>::EventLoopProxy;
206208

209+
/// Requests an exit of the event loop.
210+
fn request_exit(&self, code: i32) -> Result<()>;
211+
207212
/// Create a new window.
208213
fn create_window<F: Fn(RawWindow) + Send + 'static>(
209214
&self,

core/tauri/src/app.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use tauri_runtime::{
4141
},
4242
RuntimeInitArgs,
4343
};
44-
use tauri_utils::PackageInfo;
44+
use tauri_utils::{debug_eprintln, PackageInfo};
4545

4646
use std::{
4747
borrow::Cow,
@@ -69,12 +69,17 @@ pub type SetupHook<R> =
6969
/// A closure that is run every time a page starts or finishes loading.
7070
pub type OnPageLoad<R> = dyn Fn(&Webview<R>, &PageLoadPayload<'_>) + Send + Sync + 'static;
7171

72+
/// The exit code on [`RunEvent::ExitRequested`] when [`AppHandle#method.restart`] is called.
73+
pub const RESTART_EXIT_CODE: i32 = i32::MAX;
74+
7275
/// Api exposed on the `ExitRequested` event.
7376
#[derive(Debug)]
7477
pub struct ExitRequestApi(Sender<ExitRequestedEventAction>);
7578

7679
impl ExitRequestApi {
77-
/// Prevents the app from exiting
80+
/// Prevents the app from exiting.
81+
///
82+
/// **Note:** This is ignored when using [`AppHandle#method.restart`].
7883
pub fn prevent_exit(&self) {
7984
self.0.send(ExitRequestedEventAction::Prevent).unwrap();
8085
}
@@ -171,6 +176,10 @@ pub enum RunEvent {
171176
/// The app is about to exit
172177
#[non_exhaustive]
173178
ExitRequested {
179+
/// Exit code.
180+
/// [`Option::None`] when the exit is requested by user interaction,
181+
/// [`Option::Some`] when requested programatically via [`AppHandle#method.exit`] and [`AppHandle#method.restart`].
182+
code: Option<i32>,
174183
/// Event API
175184
api: ExitRequestApi,
176185
},
@@ -365,15 +374,20 @@ impl<R: Runtime> AppHandle<R> {
365374
self.manager().plugins.lock().unwrap().unregister(plugin)
366375
}
367376

368-
/// Exits the app. This is the same as [`std::process::exit`], but it performs cleanup on this application.
377+
/// Exits the app by triggering [`RunEvent::ExitRequested`] and [`RunEvent::Exit`].
369378
pub fn exit(&self, exit_code: i32) {
370-
self.cleanup_before_exit();
371-
std::process::exit(exit_code);
379+
if let Err(e) = self.runtime_handle.request_exit(exit_code) {
380+
debug_eprintln!("failed to exit: {}", e);
381+
self.cleanup_before_exit();
382+
std::process::exit(exit_code);
383+
}
372384
}
373385

374-
/// Restarts the app. This is the same as [`crate::process::restart`], but it performs cleanup on this application.
386+
/// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`] and [`RunEvent::Exit`]..
375387
pub fn restart(&self) {
376-
self.cleanup_before_exit();
388+
if self.runtime_handle.request_exit(RESTART_EXIT_CODE).is_err() {
389+
self.cleanup_before_exit();
390+
}
377391
crate::process::restart(&self.env());
378392
}
379393
}
@@ -1718,7 +1732,8 @@ fn on_event_loop_event<R: Runtime>(
17181732

17191733
let event = match event {
17201734
RuntimeRunEvent::Exit => RunEvent::Exit,
1721-
RuntimeRunEvent::ExitRequested { tx } => RunEvent::ExitRequested {
1735+
RuntimeRunEvent::ExitRequested { code, tx } => RunEvent::ExitRequested {
1736+
code,
17221737
api: ExitRequestApi(tx),
17231738
},
17241739
RuntimeRunEvent::WindowEvent { label, event } => RunEvent::WindowEvent {

core/tauri/src/test/mock_runtime.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
118118
EventProxy {}
119119
}
120120

121+
fn request_exit(&self, code: i32) -> Result<()> {
122+
unimplemented!()
123+
}
124+
121125
/// Create a new webview window.
122126
fn create_window<F: Fn(RawWindow<'_>) + Send + 'static>(
123127
&self,
@@ -1008,7 +1012,7 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
10081012
let is_empty = self.context.windows.borrow().is_empty();
10091013
if is_empty {
10101014
let (tx, rx) = channel();
1011-
callback(RunEvent::ExitRequested { tx });
1015+
callback(RunEvent::ExitRequested { code: None, tx });
10121016

10131017
let recv = rx.try_recv();
10141018
let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent));

core/tauri/src/window/mod.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,12 +1569,6 @@ impl<R: Runtime> Window<R> {
15691569
}
15701570

15711571
/// Closes this window.
1572-
/// # Panics
1573-
///
1574-
/// - Panics if the event loop is not running yet, usually when called on the [`setup`](crate::Builder#method.setup) closure.
1575-
/// - Panics when called on the main thread, usually on the [`run`](crate::App#method.run) closure.
1576-
///
1577-
/// You can spawn a task to use the API using [`crate::async_runtime::spawn`] or [`std::thread::spawn`] to prevent the panic.
15781572
pub fn close(&self) -> crate::Result<()> {
15791573
self.window.dispatcher.close().map_err(Into::into)
15801574
}

examples/api/src-tauri/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,13 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
153153

154154
app.run(move |_app_handle, _event| {
155155
#[cfg(all(desktop, not(test)))]
156-
if let RunEvent::ExitRequested { api, .. } = &_event {
156+
if let RunEvent::ExitRequested { api, code, .. } = &_event {
157157
// Keep the event loop running even if all windows are closed
158158
// This allow us to catch tray icon events when there is no window
159-
api.prevent_exit();
159+
// if we manually requested an exit (code is Some(_)) we will let it go through
160+
if code.is_none() {
161+
api.prevent_exit();
162+
}
160163
}
161164
})
162165
}

0 commit comments

Comments
 (0)