mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
refactor(gui-client): gracefully exit Tauri app (#7959)
At present, the Windows and Linux GUI client launch the Tauri application via the `App::run` method. This function never returns again. Instead, whenever we request the Tauri app to exit, Tauri will internally call `std::process::exit`, thus preventing ordinary clean-up from happening. Whilst we somehow managed to work around this particular part, having the app exit the process internally also makes error handling and reporting to the user difficult as there are now two parts in the code where we need to handle errors: - Before we start up the Tauri app - Before we end the Tauri app (i.e. signal to it that we want to exit) It would be much easier to understand, if we could call into Tauri, let it do its thing and upon a requested exit by the user, the called function (i.e. `App::run`) simply returns again. After diving into the inner workings of Tauri, we have achieved just that by adding a new function to `App`: `App::run_return` (https://github.com/tauri-apps/tauri/pull/12668). Using `App::run_return` we can now orchestrate a `gui::run` function that simply returns after Tauri has shutdown. Most importantly, it will also exit upon any fatal errors that we encounter in the controller and thus unify the error handling path into a single one. These errors are now all handled at the call-site of `gui::run`. Building on top of this, we will be able to further simplify the error handling within the GUI client. I am hoping to gradually replace our monolithic `Error` enums with individual errors that we can extract from an `anyhow::Error`. This would make it easier to reason about where certain errors get generated and thus overall improve the UX of the application by displaying better error messages, not failing the entire app in certain cases, etc.
This commit is contained in:
38
rust/Cargo.lock
generated
38
rust/Cargo.lock
generated
@@ -777,9 +777,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cargo_toml"
|
||||
version = "0.21.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472"
|
||||
checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml",
|
||||
@@ -6504,9 +6504,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3be747b26bf28674977fac47bdf6963fd9c7578271c3fbeb25d8686de6596f35"
|
||||
checksum = "511dd38065a5d3b36c33cdba4362b99a40a5103bebcd4aebb930717e7c8ba292"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -6555,9 +6555,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.6"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51a2e96f3c0baa0581656bb58e6fdd0f7c9c31eaf6721a0c08689d938fe85f2d"
|
||||
checksum = "7ffa8732a66f90903f5a585215f3cf1e87988d0359bc88c18a502efe7572c1de"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -6577,9 +6577,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.5"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e357ec3daf8faad1029bc7109e7f5b308ceb63b6073d110d7388923a4cce5e55"
|
||||
checksum = "c266a247f14d63f40c6282c2653a8bac5cc3d482ca562a003a88513653ea817a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -6604,9 +6604,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.5"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "447ee4dd94690d77f1422f2b57e783c654ba75c535ad6f6e727887330804fff2"
|
||||
checksum = "f47a1cf94b3bd6c4dc37dce1a43fc96120ff29a91757f0ab3cf713c7ad846e7c"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -6618,9 +6618,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e6660a409963e4d57b9bfab4addd141eeff41bd3a7fb14e13004a832cf7ef6"
|
||||
checksum = "9972871fcbddf16618f70412d965d4d845cd4b76d03fff168709961ef71e5cdf"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@@ -6738,10 +6738,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e758a405ab39e25f4d1235c5f06fe563f44b01ee18bbe38ddec5356d4f581908"
|
||||
checksum = "9e9c7bce5153f1ca7bc45eba37349b31ba50e975e28edc8b5766c5ec02b0b63a"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -6757,9 +6758,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2beb90decade4c71e8b09c9e4a9245837a8a97693f945b77e32baf13f51fec"
|
||||
checksum = "087188020fd6facb8578fe9b38e81fa0fe5fb85744c73da51a299f94a530a1e3"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -6784,10 +6785,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "107a959dbd5ff53d89a98f6f2e3e987c611334141a43630caae1d80e79446dd6"
|
||||
checksum = "82dcced4014e59af9790cc22f5d271df3be09ecd6728ec68861642553c8d01b7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
"ctor",
|
||||
|
||||
@@ -139,13 +139,13 @@ subtle = "2.5.0"
|
||||
supports-color = "3.0.2"
|
||||
swift-bridge = "0.1.57"
|
||||
swift-bridge-build = "0.1.57"
|
||||
tauri = "2.3.1"
|
||||
tauri-build = "2.0.6"
|
||||
tauri = "2.4.0"
|
||||
tauri-build = "2.1.0"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-notification = "2.2.2"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
tauri-plugin-shell = "2.2.0"
|
||||
tauri-runtime = "2.4.0"
|
||||
tauri-runtime = "2.5.0"
|
||||
tauri-utils = "2.2.0"
|
||||
tempfile = "3.13.0"
|
||||
test-case = "3.3.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use clap::{Args, Parser};
|
||||
use firezone_gui_client_common::{
|
||||
self as common, controller::Failure, deep_link, settings::AdvancedSettings,
|
||||
self as common, controller::Failure, deep_link, errors, settings::AdvancedSettings,
|
||||
};
|
||||
use firezone_telemetry::Telemetry;
|
||||
use tracing::instrument;
|
||||
@@ -129,6 +129,13 @@ fn run_gui(cli: Cli) -> Result<()> {
|
||||
return Err(anyhow);
|
||||
}
|
||||
|
||||
// TODO: Get rid of `errors::Error` and check for sources individually like above.
|
||||
if let Some(error) = anyhow.root_cause().downcast_ref::<errors::Error>() {
|
||||
common::errors::show_error_dialog(error.user_friendly_msg())?;
|
||||
tracing::error!("GUI failed: {anyhow:#}");
|
||||
return Err(anyhow);
|
||||
}
|
||||
|
||||
common::errors::show_error_dialog(common::errors::GENERIC_MSG.to_owned())?;
|
||||
tracing::error!("GUI failed: {anyhow:#}");
|
||||
|
||||
|
||||
@@ -12,15 +12,14 @@ use common::system_tray::Event as TrayMenuEvent;
|
||||
use firezone_gui_client_common::{
|
||||
self as common,
|
||||
controller::{Controller, ControllerRequest, CtlrTx, GuiIntegration},
|
||||
deep_link, errors,
|
||||
deep_link,
|
||||
settings::AdvancedSettings,
|
||||
updates,
|
||||
};
|
||||
use firezone_logging::err_with_src;
|
||||
use firezone_telemetry as telemetry;
|
||||
use futures::FutureExt;
|
||||
use secrecy::{ExposeSecret as _, SecretString};
|
||||
use std::{panic::AssertUnwindSafe, str::FromStr, time::Duration};
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::Manager;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::instrument;
|
||||
@@ -57,6 +56,14 @@ struct TauriIntegration {
|
||||
tray: system_tray::Tray,
|
||||
}
|
||||
|
||||
impl Drop for TauriIntegration {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("Instructing Tauri to exit");
|
||||
|
||||
self.app.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiIntegration for TauriIntegration {
|
||||
fn set_welcome_window_visible(&self, visible: bool) -> Result<()> {
|
||||
let win = self
|
||||
@@ -139,6 +146,8 @@ pub(crate) fn run(
|
||||
inject_faults: cli.inject_faults,
|
||||
};
|
||||
|
||||
let (handle_tx, handle_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.manage(managed)
|
||||
.on_window_event(|window, event| {
|
||||
@@ -233,70 +242,54 @@ pub(crate) fn run(
|
||||
})?;
|
||||
let integration = TauriIntegration { app: app.handle().clone(), tray };
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
let _ctlr_task = tokio::spawn(async move {
|
||||
let result = AssertUnwindSafe(Controller::start(
|
||||
ctlr_tx,
|
||||
integration,
|
||||
ctlr_rx,
|
||||
advanced_settings,
|
||||
reloader,
|
||||
updates_rx,
|
||||
)).catch_unwind().await;
|
||||
// Spawn the controller
|
||||
let ctrl_task = tokio::spawn(Controller::start(
|
||||
ctlr_tx,
|
||||
integration,
|
||||
ctlr_rx,
|
||||
advanced_settings,
|
||||
reloader,
|
||||
updates_rx,
|
||||
));
|
||||
|
||||
// See <https://github.com/tauri-apps/tauri/issues/8631>
|
||||
// This should be the ONLY place we call `app.exit` or `app_handle.exit`,
|
||||
// because it exits the entire process without dropping anything.
|
||||
//
|
||||
// This seems to be a platform limitation that Tauri is unable to hide
|
||||
// from us. It was the source of much consternation at time of writing.
|
||||
|
||||
let exit_code = match result {
|
||||
Err(_panic) => {
|
||||
// The panic will have been recorded already by Sentry's panic hook.
|
||||
telemetry.stop_on_crash().await;
|
||||
|
||||
1
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
tracing::error!("run_controller returned an error: {}", err_with_src(&error));
|
||||
if let Err(e) = errors::show_error_dialog(error.user_friendly_msg()) {
|
||||
tracing::error!("Failed to show error dialog: {e:#}");
|
||||
}
|
||||
telemetry.stop_on_crash().await;
|
||||
1
|
||||
}
|
||||
Ok(Ok(_)) => {
|
||||
telemetry.stop().await;
|
||||
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
// But due to a limit in `tao` we cannot return from the event loop and must call `std::process::exit` (or Tauri's wrapper), so we explicitly flush here.
|
||||
// TODO: This limit may not exist in Tauri v2
|
||||
|
||||
tracing::info!(%exit_code, "Goodbye!");
|
||||
app_handle.exit(exit_code);
|
||||
// In Tauri v1, calling `App::exit` internally exited the process.
|
||||
// In Tauri v2, that doesn't happen, but `App::run` still doesn't return, so we have to bail out of the process manually.
|
||||
std::process::exit(exit_code);
|
||||
});
|
||||
// Send the handle to the controller task back to the main thread.
|
||||
// If the receiver is gone, we don't care.
|
||||
let _ = handle_tx.send(ctrl_task);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.context("Failed to build Tauri app instance")?;
|
||||
|
||||
app.run(|_app_handle, event| {
|
||||
if let tauri::RunEvent::ExitRequested { api, .. } = event {
|
||||
// Run the Tauri app to completion, i.e. until `app_handle.exit(0)` is called.
|
||||
app.run_return(|_app_handle, event| {
|
||||
if let tauri::RunEvent::ExitRequested {
|
||||
api, code: None, .. // `code: None` means the user closed the last window.
|
||||
} = event
|
||||
{
|
||||
// Don't exit if we close our main window
|
||||
// https://tauri.app/v1/guides/features/system-tray/#preventing-the-app-from-closing
|
||||
|
||||
api.prevent_exit();
|
||||
}
|
||||
});
|
||||
tracing::warn!("app.run returned, this is normally unreachable even in Tauri v2");
|
||||
|
||||
// Wait until the controller task finishes.
|
||||
rt.block_on(async move {
|
||||
let ctrl_task = handle_rx.await.context("Failed to complete setup hook")?;
|
||||
let ctrl_or_timeout = tokio::time::timeout(Duration::from_secs(5), ctrl_task);
|
||||
|
||||
ctrl_or_timeout
|
||||
.await
|
||||
.context("Controller failed to exit within 5s after OS-eventloop finished")?
|
||||
.context("Controller panicked")?
|
||||
.context("Controller failed")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.inspect_err(|_| rt.block_on(telemetry.stop_on_crash()))?;
|
||||
|
||||
tracing::info!("Controller exited gracefully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user