From 52c77167c9215a8e232e5ac82542492c30be5b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 8 Jul 2025 14:28:04 +0200 Subject: [PATCH 1/9] enable WAL journal mode --- src-tauri/src/database/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index d2f2fdc4..e5260254 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -3,8 +3,11 @@ pub mod models; use std::{ env, fs::{create_dir_all, File}, + str::FromStr, }; +use sqlx::sqlite::SqliteConnectOptions; + use crate::{app_data_dir, error::Error}; const DB_NAME: &str = "defguard.db"; @@ -14,8 +17,10 @@ pub(crate) type DbPool = sqlx::SqlitePool; /// Initializes the database pub fn init_db() -> Result { let db_url = prepare_db_url()?; - debug!("Connecting to database: {db_url}"); - let pool = DbPool::connect_lazy(&db_url)?; + let opts = + SqliteConnectOptions::from_str(&db_url)?.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); + println!("Connecting to database: {db_url} with options: {opts:?}"); + let pool = DbPool::connect_lazy_with(opts); Ok(pool) } From b44512b5d156a6058fa3603a109ae166208c7008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 8 Jul 2025 15:43:09 +0200 Subject: [PATCH 2/9] don't purge stats on each update --- src-tauri/src/utils.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 41ea2932..4b73e958 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -210,13 +210,6 @@ pub(crate) async fn stats_handler( match stream.message().await { Ok(Some(interface_data)) => { debug!("Received new network usage statistics for interface {interface_name}."); - if let Err(err) = LocationStats::purge(&pool).await { - error!("Failed to purge location stats: {err}"); - } - if let Err(err) = TunnelStats::purge(&pool).await { - error!("Failed to purge tunnel stats: {err}"); - } - trace!("Received interface data: {interface_data:?}"); let peers: Vec = interface_data.peers.into_iter().map(Into::into).collect(); for peer in peers { From 124b7f90552f5e63572671c900a3c04349c8618e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 8 Jul 2025 20:47:57 +0200 Subject: [PATCH 3/9] run stats purge periodically --- src-tauri/src/periodic/mod.rs | 5 +++ src-tauri/src/periodic/purge_stats.rs | 58 +++++++++++++++++++++++++++ src-tauri/src/utils.rs | 4 +- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src-tauri/src/periodic/purge_stats.rs diff --git a/src-tauri/src/periodic/mod.rs b/src-tauri/src/periodic/mod.rs index f3df75a1..4a903d59 100644 --- a/src-tauri/src/periodic/mod.rs +++ b/src-tauri/src/periodic/mod.rs @@ -1,4 +1,5 @@ use connection::verify_active_connections; +use purge_stats::purge_stats; use tauri::AppHandle; use tokio::select; use version::poll_version; @@ -6,6 +7,7 @@ use version::poll_version; use crate::enterprise::periodic::config::poll_config; pub mod connection; +pub mod purge_stats; pub mod version; /// Runs all the client periodic tasks, finishing when any of them returns. @@ -20,5 +22,8 @@ pub async fn run_periodic_tasks(app_handle: &AppHandle) { () = verify_active_connections(app_handle.clone()) => { error!("Active connection verification task has stopped unexpectedly"); } + () = purge_stats(app_handle.clone()) => { + error!("Stats purging task has stopped unexpectedly"); + } }; } diff --git a/src-tauri/src/periodic/purge_stats.rs b/src-tauri/src/periodic/purge_stats.rs new file mode 100644 index 00000000..84f0613b --- /dev/null +++ b/src-tauri/src/periodic/purge_stats.rs @@ -0,0 +1,58 @@ +use std::time::Duration; + +use tauri::{AppHandle, Manager, State}; +use tokio::time::interval; + +use crate::{ + appstate::AppState, + database::models::{location_stats::LocationStats, tunnel::TunnelStats}, +}; + +// 12 hours +const PURGE_INTERVAL: Duration = Duration::from_secs(12 * 60 * 60); + +/// Periodically purges location and tunnel stats. +/// +/// By design this happens infrequently to not overload the DB connection. +/// There is a separate purge done at client startup. +pub async fn purge_stats(handle: AppHandle) { + debug!("Starting the stats purging loop..."); + let app_state: State = handle.state(); + let pool = &app_state.db; + + let mut interval = interval(PURGE_INTERVAL); + + loop { + // wait for next iteration + interval.tick().await; + + // begin transaction + let Ok(mut transaction) = pool.begin().await else { + error!( + "Failed to begin database transaction for stats purging, retrying in {}h", + PURGE_INTERVAL.as_secs() / 3600 + ); + continue; + }; + + debug!("Purging old stats from the database..."); + if let Err(err) = LocationStats::purge(&mut *transaction).await { + error!("Failed to purge location stats: {err}"); + } else { + debug!("Old location stats have been purged successfully."); + } + if let Err(err) = TunnelStats::purge(&mut *transaction).await { + error!("Failed to purge tunnel stats: {err}"); + } else { + debug!("Old tunnel stats have been purged successfully."); + } + + // commit transaction + if let Err(err) = transaction.commit().await { + error!( + "Failed to commit database transaction for stats purging: {err}. Retrying in {}h", + PURGE_INTERVAL.as_secs() / 3600 + ) + } + } +} diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 4b73e958..e57ea6e2 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -21,8 +21,8 @@ use crate::{ models::{ connection::{ActiveConnection, Connection}, location::Location, - location_stats::{peer_to_location_stats, LocationStats}, - tunnel::{peer_to_tunnel_stats, Tunnel, TunnelConnection, TunnelStats}, + location_stats::peer_to_location_stats, + tunnel::{peer_to_tunnel_stats, Tunnel, TunnelConnection}, wireguard_keys::WireguardKeys, Id, }, From f70724e48dbf1eca597af73b356d8f47d3b9d5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 8 Jul 2025 20:50:47 +0200 Subject: [PATCH 4/9] update logs --- src-tauri/src/bin/defguard-client.rs | 3 --- src-tauri/src/periodic/mod.rs | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 82dc9434..dc84ba85 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -254,9 +254,6 @@ async fn main() { } // run periodic tasks - debug!( - "Starting periodic tasks (config, version polling and active connection verification)..." - ); let periodic_tasks_handle = app_handle.clone(); tauri::async_runtime::spawn(async move { run_periodic_tasks(&periodic_tasks_handle).await; diff --git a/src-tauri/src/periodic/mod.rs b/src-tauri/src/periodic/mod.rs index 4a903d59..7ab8f8fc 100644 --- a/src-tauri/src/periodic/mod.rs +++ b/src-tauri/src/periodic/mod.rs @@ -12,6 +12,9 @@ pub mod version; /// Runs all the client periodic tasks, finishing when any of them returns. pub async fn run_periodic_tasks(app_handle: &AppHandle) { + debug!( + "Starting periodic tasks (config, version polling, stats purging and active connection verification)..." + ); select! { () = poll_version(app_handle.clone()) => { error!("Version polling task has stopped unexpectedly"); From bdca8c55e570cec98962ff27c85d0a9e8739c591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 9 Jul 2025 09:57:53 +0200 Subject: [PATCH 5/9] use more transactions --- src-tauri/src/commands.rs | 21 +++++++---- src-tauri/src/utils.rs | 73 ++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 182e14d6..ad222e3f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -804,14 +804,15 @@ pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), E debug!("Deleting instance with ID {instance_id}"); let app_state = handle.state::(); let mut client = app_state.client.clone(); - let pool = &app_state.db; - let Some(instance) = Instance::find_by_id(pool, instance_id).await? else { + let mut transaction = app_state.db.begin().await?; + + let Some(instance) = Instance::find_by_id(&mut *transaction, instance_id).await? else { error!("Couldn't delete instance: instance with ID {instance_id} could not be found."); return Err(Error::NotFound); }; debug!("The instance that is being deleted has been identified as {instance}"); - let instance_locations = Location::find_by_instance_id(pool, instance_id).await?; + let instance_locations = Location::find_by_instance_id(&mut *transaction, instance_id).await?; if !instance_locations.is_empty() { debug!( "Found locations associated with the instance {instance}, closing their connections..." @@ -836,7 +837,10 @@ pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), E info!("The connection to location {location} has been closed, as it was associated with the instance {instance} that is being deleted."); } } - instance.delete(pool).await?; + instance.delete(&mut *transaction).await?; + + transaction.commit().await?; + reload_tray_menu(&handle).await; handle.emit_all(INSTANCE_UPDATE, ())?; @@ -948,8 +952,9 @@ pub async fn delete_tunnel(tunnel_id: Id, handle: AppHandle) -> Result<(), Error debug!("Deleting tunnel with ID {tunnel_id}"); let app_state = handle.state::(); let mut client = app_state.client.clone(); - let pool = &app_state.db; - let Some(tunnel) = Tunnel::find_by_id(pool, tunnel_id).await? else { + let mut transaction = app_state.db.begin().await?; + + let Some(tunnel) = Tunnel::find_by_id(&mut *transaction, tunnel_id).await? else { error!("The tunnel to delete with ID {tunnel_id} could not be found, cannot delete."); return Err(Error::NotFound); }; @@ -1015,7 +1020,9 @@ pub async fn delete_tunnel(tunnel_id: Id, handle: AppHandle) -> Result<(), Error ); } } - tunnel.delete(pool).await?; + tunnel.delete(&mut *transaction).await?; + + transaction.commit().await?; info!("Successfully deleted tunnel {tunnel}"); Ok(()) diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index e57ea6e2..c5a89032 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -211,21 +211,36 @@ pub(crate) async fn stats_handler( Ok(Some(interface_data)) => { debug!("Received new network usage statistics for interface {interface_name}."); trace!("Received interface data: {interface_data:?}"); + + // begin transaction + let mut transaction = match pool.begin().await { + Ok(transactions) => transactions, + Err(err) => { + error!( + "Failed to begin database transaction for saving location/tunnel stats: {err}", + ); + continue; + } + }; + let peers: Vec = interface_data.peers.into_iter().map(Into::into).collect(); for peer in peers { if connection_type.eq(&ConnectionType::Location) { - let location_stats = - match peer_to_location_stats(&peer, interface_data.listen_port, &pool) - .await - { - Ok(stats) => stats, - Err(err) => { - error!("Failed to convert peer data to location stats: {err}"); - continue; - } - }; + let location_stats = match peer_to_location_stats( + &peer, + interface_data.listen_port, + &mut *transaction, + ) + .await + { + Ok(stats) => stats, + Err(err) => { + error!("Failed to convert peer data to location stats: {err}"); + continue; + } + }; let location_name = location_stats - .get_name(&pool) + .get_name(&mut *transaction) .await .unwrap_or("UNKNOWN".to_string()); @@ -234,7 +249,7 @@ pub(crate) async fn stats_handler( (interface {interface_name})." ); trace!("Stats: {location_stats:?}"); - match location_stats.save(&pool).await { + match location_stats.save(&mut *transaction).await { Ok(_) => { debug!("Saved network usage stats for location {location_name}"); } @@ -246,25 +261,28 @@ pub(crate) async fn stats_handler( } } } else { - let tunnel_stats = - match peer_to_tunnel_stats(&peer, interface_data.listen_port, &pool) - .await - { - Ok(stats) => stats, - Err(err) => { - error!("Failed to convert peer data to tunnel stats: {err}"); - continue; - } - }; + let tunnel_stats = match peer_to_tunnel_stats( + &peer, + interface_data.listen_port, + &mut *transaction, + ) + .await + { + Ok(stats) => stats, + Err(err) => { + error!("Failed to convert peer data to tunnel stats: {err}"); + continue; + } + }; let tunnel_name = tunnel_stats - .get_name(&pool) + .get_name(&mut *transaction) .await .unwrap_or("UNKNOWN".to_string()); debug!( "Saving network usage stats related to tunnel {tunnel_name} \ (interface {interface_name}): {tunnel_stats:?}" ); - match tunnel_stats.save(&pool).await { + match tunnel_stats.save(&mut *transaction).await { Ok(_) => { debug!("Saved stats for tunnel {tunnel_name}"); } @@ -274,6 +292,13 @@ pub(crate) async fn stats_handler( } } } + + // commit transaction + if let Err(err) = transaction.commit().await { + error!( + "Failed to commit database transaction for saving location/tunnel stats: {err}", + ) + } } Ok(None) => { debug!("gRPC stream to the defguard-service managing connections has been closed"); From f22b176b865f8d824db1b7a2c69b755ad378af52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 9 Jul 2025 10:09:46 +0200 Subject: [PATCH 6/9] add error message --- src-tauri/src/bin/defguard-client.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index dc84ba85..9023cc3b 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -109,7 +109,11 @@ async fn main() { let app_state: State = app.state(); // use config default if deriving from env value fails so that env can override config file - let config_log_level = app_state.app_config.lock().unwrap().log_level; + let config_log_level = app_state + .app_config + .lock() + .expect("Failed to acquire lock on app config") + .log_level; let log_level = match &env::var("DEFGUARD_CLIENT_LOG_LEVEL") { Ok(env_value) => LevelFilter::from_str(env_value).unwrap_or(config_log_level), From 57654ba3a90c08248ab2639cb4deac41f6d3a306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 9 Jul 2025 10:21:03 +0200 Subject: [PATCH 7/9] initialize logging first --- src-tauri/src/appstate.rs | 4 ++-- src-tauri/src/bin/defguard-client.rs | 21 ++++++++++----------- src-tauri/src/database/mod.rs | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 90c11ed2..6435d8d8 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -34,13 +34,13 @@ pub struct AppState { impl AppState { #[must_use] - pub fn new(app_handle: &AppHandle) -> Self { + pub fn new(config: AppConfig) -> Self { AppState { db: init_db().expect("Failed to initalize database"), active_connections: Mutex::new(Vec::new()), client: setup_client().expect("Failed to setup gRPC client"), log_watchers: std::sync::Mutex::new(HashMap::new()), - app_config: std::sync::Mutex::new(AppConfig::new(app_handle)), + app_config: std::sync::Mutex::new(config), stat_threads: std::sync::Mutex::new(HashMap::new()), } } diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 9023cc3b..35f6c878 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -8,6 +8,7 @@ use std::{env, str::FromStr, sync::LazyLock}; #[cfg(target_os = "windows")] use defguard_client::utils::sync_connections; use defguard_client::{ + app_config::AppConfig, appstate::AppState, commands::*, database::models::{location_stats::LocationStats, tunnel::TunnelStats}, @@ -102,19 +103,12 @@ async fn main() { let _ = app.emit_all(SINGLE_INSTANCE, Payload { args: argv, cwd }); })) .setup(|app| { - { - let state = AppState::new(&app.app_handle()); - app.manage(state); - } - let app_state: State = app.state(); + // prepare config + let config = AppConfig::new(&app.app_handle()); + // setup logging // use config default if deriving from env value fails so that env can override config file - let config_log_level = app_state - .app_config - .lock() - .expect("Failed to acquire lock on app config") - .log_level; - + let config_log_level = config.log_level; let log_level = match &env::var("DEFGUARD_CLIENT_LOG_LEVEL") { Ok(env_value) => LevelFilter::from_str(env_value).unwrap_or(config_log_level), Err(_) => config_log_level, @@ -177,6 +171,11 @@ async fn main() { ) .unwrap(); + { + let state = AppState::new(config); + app.manage(state); + } + info!("App setup completed, log level: {log_level}"); Ok(()) diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index e5260254..98a51c53 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -19,7 +19,7 @@ pub fn init_db() -> Result { let db_url = prepare_db_url()?; let opts = SqliteConnectOptions::from_str(&db_url)?.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); - println!("Connecting to database: {db_url} with options: {opts:?}"); + debug!("Connecting to database: {db_url} with options: {opts:?}"); let pool = DbPool::connect_lazy_with(opts); Ok(pool) From 623891adf6bf335d26bcf7bc7ebae4a9eb2dacff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 9 Jul 2025 11:01:32 +0200 Subject: [PATCH 8/9] enable incremental auto-vacuum --- src-tauri/src/database/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 98a51c53..db4525dc 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -17,8 +17,9 @@ pub(crate) type DbPool = sqlx::SqlitePool; /// Initializes the database pub fn init_db() -> Result { let db_url = prepare_db_url()?; - let opts = - SqliteConnectOptions::from_str(&db_url)?.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); + let opts = SqliteConnectOptions::from_str(&db_url)? + .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Incremental) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); debug!("Connecting to database: {db_url} with options: {opts:?}"); let pool = DbPool::connect_lazy_with(opts); From d55cd9357b0a762deb36fb5fc99468509f0fa005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 9 Jul 2025 12:21:42 +0200 Subject: [PATCH 9/9] adjust imports --- src-tauri/src/periodic/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/periodic/mod.rs b/src-tauri/src/periodic/mod.rs index 7ab8f8fc..239da3e2 100644 --- a/src-tauri/src/periodic/mod.rs +++ b/src-tauri/src/periodic/mod.rs @@ -1,8 +1,8 @@ -use connection::verify_active_connections; -use purge_stats::purge_stats; +use self::{ + connection::verify_active_connections, purge_stats::purge_stats, version::poll_version, +}; use tauri::AppHandle; use tokio::select; -use version::poll_version; use crate::enterprise::periodic::config::poll_config;