From 7bf98e2fdc91b638fb7e01573d48901ee923b96d Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Tue, 3 Jun 2025 23:39:04 +0200 Subject: [PATCH] refactor(gui-client): improve error handling of Tauri commands (#9380) Introducing a dedicated `Error` type allows us to directly serialise `anyhow::Error`s to the GUI. Those will then be reported to Sentry on the TS side. --- rust/gui-client/src-tauri/src/view.rs | 87 +++++++++++++++------------ 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/rust/gui-client/src-tauri/src/view.rs b/rust/gui-client/src-tauri/src/view.rs index 31cb96e15..1a9e214bf 100644 --- a/rust/gui-client/src-tauri/src/view.rs +++ b/rust/gui-client/src-tauri/src/view.rs @@ -1,7 +1,8 @@ use std::{path::PathBuf, time::Duration}; -use anyhow::{Context as _, Result, bail}; +use anyhow::Context as _; use firezone_logging::err_with_src; +use serde::Serialize; use tauri::{Wry, ipc::Invoke}; use tauri_plugin_dialog::DialogExt as _; @@ -24,39 +25,34 @@ pub fn generate_handler() -> impl Fn(Invoke) -> bool + Send + Sync + 'stati } #[tauri::command] -async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<(), String> { +async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<()> { let (tx, rx) = tokio::sync::oneshot::channel(); - if let Err(error) = managed.ctlr_tx.send(ControllerRequest::ClearLogs(tx)).await { - // Tauri will only log errors to the JS console for us, so log this ourselves. - tracing::error!( - "Error while asking `Controller` to clear logs: {}", - err_with_src(&error) - ); - return Err(error.to_string()); - } - if let Err(error) = rx.await { - tracing::error!( - "Error while awaiting log-clearing operation: {}", - err_with_src(&error) - ); - return Err(error.to_string()); - } + + managed + .ctlr_tx + .send(ControllerRequest::ClearLogs(tx)) + .await + .context("Failed to send `ClearLogs` command")?; + + rx.await + .context("Failed to await `ClearLogs` result")? + .map_err(anyhow::Error::msg)?; + Ok(()) } #[tauri::command] -async fn export_logs( - app: tauri::AppHandle, - managed: tauri::State<'_, Managed>, -) -> Result<(), String> { - show_export_dialog(&app, managed.ctlr_tx.clone()).map_err(|e| e.to_string()) +async fn export_logs(app: tauri::AppHandle, managed: tauri::State<'_, Managed>) -> Result<()> { + show_export_dialog(&app, managed.ctlr_tx.clone())?; + + Ok(()) } #[tauri::command] async fn apply_advanced_settings( managed: tauri::State<'_, Managed>, settings: AdvancedSettings, -) -> Result<(), String> { +) -> Result<()> { if managed.inner().inject_faults { tokio::time::sleep(Duration::from_secs(2)).await; } @@ -65,13 +61,13 @@ async fn apply_advanced_settings( .ctlr_tx .send(ControllerRequest::ApplySettings(Box::new(settings))) .await - .map_err(|e| e.to_string())?; + .context("Failed to send `ApplySettings` command")?; Ok(()) } #[tauri::command] -async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<(), String> { +async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<()> { apply_advanced_settings(managed, AdvancedSettings::default()).await?; Ok(()) @@ -83,9 +79,9 @@ fn show_export_dialog(app: &tauri::AppHandle, ctlr_tx: CtlrTx) -> Result<()> { let datetime_string = now.format("%Y_%m_%d-%H-%M"); let stem = PathBuf::from(format!("firezone_logs_{datetime_string}")); let filename = stem.with_extension("zip"); - let Some(filename) = filename.to_str() else { - bail!("zip filename isn't valid Unicode"); - }; + let filename = filename + .to_str() + .context("zip filename isn't valid Unicode")?; tauri_plugin_dialog::FileDialogBuilder::new(app.dialog().clone()) .add_filter("Zip", &["zip"]) @@ -112,37 +108,54 @@ fn show_export_dialog(app: &tauri::AppHandle, ctlr_tx: CtlrTx) -> Result<()> { } #[tauri::command] -async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<(), String> { +async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<()> { managed .ctlr_tx .send(ControllerRequest::SignIn) .await - .context("Failed to send `SignIn` command") - .map_err(|e| e.to_string())?; + .context("Failed to send `SignIn` command")?; Ok(()) } #[tauri::command] -async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<(), String> { +async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<()> { managed .ctlr_tx .send(ControllerRequest::SignOut) .await - .context("Failed to send `SignOut` command") - .map_err(|e| e.to_string())?; + .context("Failed to send `SignOut` command")?; Ok(()) } #[tauri::command] -async fn update_state(managed: tauri::State<'_, Managed>) -> Result<(), String> { +async fn update_state(managed: tauri::State<'_, Managed>) -> Result<()> { managed .ctlr_tx .send(ControllerRequest::UpdateState) .await - .context("Failed to send `UpdateState` command") - .map_err(|e| e.to_string())?; + .context("Failed to send `UpdateState` command")?; Ok(()) } + +type Result = std::result::Result; + +#[derive(Debug)] +struct Error(anyhow::Error); + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("{:#}", self.0)) + } +} + +impl From for Error { + fn from(value: anyhow::Error) -> Self { + Self(value) + } +}