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:
Thomas Eizinger
2025-03-25 20:55:33 +11:00
committed by GitHub
parent edb9534f1f
commit c31c2ef56d
4 changed files with 79 additions and 77 deletions

38
rust/Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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:#}");

View File

@@ -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(())
}