From 00e1567584721644797b587205187f9cbe4e5cd1 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 30 Jan 2024 12:10:22 -0300 Subject: [PATCH] refactor(core): exit() and restart() triggers ExitRequested and Exit (#8708) * refactor(core): exit() and restart() triggers ExitRequested and Exit * update docs * update doc --- .changes/refactor-exit.md | 5 +++++ .changes/request-exit.md | 6 ++++++ core/tauri-runtime-wry/src/lib.rs | 27 ++++++++++++++++++++++++- core/tauri-runtime/src/lib.rs | 5 +++++ core/tauri/src/app.rs | 31 +++++++++++++++++++++-------- core/tauri/src/test/mock_runtime.rs | 6 +++++- core/tauri/src/window/mod.rs | 6 ------ examples/api/src-tauri/src/lib.rs | 7 +++++-- 8 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 .changes/refactor-exit.md create mode 100644 .changes/request-exit.md diff --git a/.changes/refactor-exit.md b/.changes/refactor-exit.md new file mode 100644 index 000000000..5ac8e6bb3 --- /dev/null +++ b/.changes/refactor-exit.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +`AppHandle::exit` and `AppHandle::restart` now go triggers `RunEvent::ExitRequested` and `RunEvent::Exit` and cannot be executed on the event loop handler. diff --git a/.changes/request-exit.md b/.changes/request-exit.md new file mode 100644 index 000000000..8f4064058 --- /dev/null +++ b/.changes/request-exit.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": patch:feat +"tauri-runtime-wry": patch:feat +--- + +Added `RuntimeHandle::request_exit` function. diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 8b88537b9..71368ca8e 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -1185,6 +1185,7 @@ pub type CreateWebviewClosure = Box Result { Task(Box), + RequestExit(i32), #[cfg(target_os = "macos")] Application(ApplicationMessage), Window(WindowId, WindowMessage), @@ -1969,6 +1970,15 @@ impl RuntimeHandle for WryHandle { EventProxy(self.context.proxy.clone()) } + fn request_exit(&self, code: i32) -> Result<()> { + // NOTE: request_exit cannot use the `send_user_message` function because it accesses the event loop callback + self + .context + .proxy + .send_event(Message::RequestExit(code)) + .map_err(|_| Error::FailedToSendMessage) + } + // Creates a window by dispatching a message to the event loop. // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. fn create_window( @@ -2411,6 +2421,7 @@ fn handle_user_message( } = context; match message { Message::Task(task) => task(), + Message::RequestExit(_code) => panic!("cannot handle RequestExit on the main thread"), #[cfg(target_os = "macos")] Message::Application(application_message) => match application_message { ApplicationMessage::Show => { @@ -2949,7 +2960,7 @@ fn handle_event_loop( let is_empty = windows.borrow().is_empty(); if is_empty { let (tx, rx) = channel(); - callback(RunEvent::ExitRequested { tx }); + callback(RunEvent::ExitRequested { code: None, tx }); let recv = rx.try_recv(); let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); @@ -2980,6 +2991,20 @@ fn handle_event_loop( } } Event::UserEvent(message) => match message { + Message::RequestExit(code) => { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { + code: Some(code), + tx, + }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + *control_flow = ControlFlow::Exit; + } + } Message::Window(id, WindowMessage::Close) => { on_window_close(id, windows.clone()); } diff --git a/core/tauri-runtime/src/lib.rs b/core/tauri-runtime/src/lib.rs index 2d5ceb121..cc572b297 100644 --- a/core/tauri-runtime/src/lib.rs +++ b/core/tauri-runtime/src/lib.rs @@ -153,6 +153,8 @@ pub enum RunEvent { Exit, /// Event loop is about to exit ExitRequested { + /// The exit code. + code: Option, tx: Sender, }, /// An event associated with a window. @@ -204,6 +206,9 @@ pub trait RuntimeHandle: Debug + Clone + Send + Sync + Sized + 'st /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. fn create_proxy(&self) -> >::EventLoopProxy; + /// Requests an exit of the event loop. + fn request_exit(&self, code: i32) -> Result<()>; + /// Create a new window. fn create_window( &self, diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index c91d33936..de3046c95 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -41,7 +41,7 @@ use tauri_runtime::{ }, RuntimeInitArgs, }; -use tauri_utils::PackageInfo; +use tauri_utils::{debug_eprintln, PackageInfo}; use std::{ borrow::Cow, @@ -69,12 +69,17 @@ pub type SetupHook = /// A closure that is run every time a page starts or finishes loading. pub type OnPageLoad = dyn Fn(&Webview, &PageLoadPayload<'_>) + Send + Sync + 'static; +/// The exit code on [`RunEvent::ExitRequested`] when [`AppHandle#method.restart`] is called. +pub const RESTART_EXIT_CODE: i32 = i32::MAX; + /// Api exposed on the `ExitRequested` event. #[derive(Debug)] pub struct ExitRequestApi(Sender); impl ExitRequestApi { - /// Prevents the app from exiting + /// 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(); } @@ -171,6 +176,10 @@ pub enum RunEvent { /// The app is about to exit #[non_exhaustive] ExitRequested { + /// Exit code. + /// [`Option::None`] when the exit is requested by user interaction, + /// [`Option::Some`] when requested programatically via [`AppHandle#method.exit`] and [`AppHandle#method.restart`]. + code: Option, /// Event API api: ExitRequestApi, }, @@ -365,15 +374,20 @@ impl AppHandle { self.manager().plugins.lock().unwrap().unregister(plugin) } - /// Exits the app. This is the same as [`std::process::exit`], but it performs cleanup on this application. + /// Exits the app by triggering [`RunEvent::ExitRequested`] and [`RunEvent::Exit`]. pub fn exit(&self, exit_code: i32) { - self.cleanup_before_exit(); - std::process::exit(exit_code); + if let Err(e) = self.runtime_handle.request_exit(exit_code) { + debug_eprintln!("failed to exit: {}", e); + self.cleanup_before_exit(); + std::process::exit(exit_code); + } } - /// Restarts the app. This is the same as [`crate::process::restart`], but it performs cleanup on this application. + /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`] and [`RunEvent::Exit`].. pub fn restart(&self) { - self.cleanup_before_exit(); + if self.runtime_handle.request_exit(RESTART_EXIT_CODE).is_err() { + self.cleanup_before_exit(); + } crate::process::restart(&self.env()); } } @@ -1718,7 +1732,8 @@ fn on_event_loop_event( let event = match event { RuntimeRunEvent::Exit => RunEvent::Exit, - RuntimeRunEvent::ExitRequested { tx } => RunEvent::ExitRequested { + RuntimeRunEvent::ExitRequested { code, tx } => RunEvent::ExitRequested { + code, api: ExitRequestApi(tx), }, RuntimeRunEvent::WindowEvent { label, event } => RunEvent::WindowEvent { diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index d7472c1bd..e049c1688 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -118,6 +118,10 @@ impl RuntimeHandle for MockRuntimeHandle { EventProxy {} } + fn request_exit(&self, code: i32) -> Result<()> { + unimplemented!() + } + /// Create a new webview window. fn create_window) + Send + 'static>( &self, @@ -1008,7 +1012,7 @@ impl Runtime for MockRuntime { let is_empty = self.context.windows.borrow().is_empty(); if is_empty { let (tx, rx) = channel(); - callback(RunEvent::ExitRequested { tx }); + callback(RunEvent::ExitRequested { code: None, tx }); let recv = rx.try_recv(); let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs index 438abfc8c..c5f21d91c 100644 --- a/core/tauri/src/window/mod.rs +++ b/core/tauri/src/window/mod.rs @@ -1569,12 +1569,6 @@ impl Window { } /// Closes this window. - /// # Panics - /// - /// - Panics if the event loop is not running yet, usually when called on the [`setup`](crate::Builder#method.setup) closure. - /// - Panics when called on the main thread, usually on the [`run`](crate::App#method.run) closure. - /// - /// You can spawn a task to use the API using [`crate::async_runtime::spawn`] or [`std::thread::spawn`] to prevent the panic. pub fn close(&self) -> crate::Result<()> { self.window.dispatcher.close().map_err(Into::into) } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index e6153b465..c9f5ba095 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -153,10 +153,13 @@ pub fn run_app) + Send + 'static>( app.run(move |_app_handle, _event| { #[cfg(all(desktop, not(test)))] - if let RunEvent::ExitRequested { api, .. } = &_event { + if let RunEvent::ExitRequested { api, code, .. } = &_event { // Keep the event loop running even if all windows are closed // This allow us to catch tray icon events when there is no window - api.prevent_exit(); + // if we manually requested an exit (code is Some(_)) we will let it go through + if code.is_none() { + api.prevent_exit(); + } } }) }