diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b32a9eab2..ecaac4d33 100755 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2025,6 +2025,7 @@ version = "1.0.0" dependencies = [ "anyhow", "arboard", + "chrono", "clap", "connlib-client-shared", "connlib-shared", diff --git a/rust/windows-client/src-tauri/Cargo.toml b/rust/windows-client/src-tauri/Cargo.toml index da672831c..c2ce9363c 100755 --- a/rust/windows-client/src-tauri/Cargo.toml +++ b/rust/windows-client/src-tauri/Cargo.toml @@ -12,6 +12,7 @@ tauri-build = { version = "1.5", features = [] } [dependencies] arboard = { version = "3.3.0", default-features = false } anyhow = { version = "1.0" } +chrono = { workspace = true } clap = { version = "4.4", features = ["derive", "env"] } connlib-client-shared = { workspace = true } connlib-shared = { workspace = true } @@ -20,7 +21,7 @@ firezone-cli-utils = { workspace = true } ipconfig = "0.3.2" keyring = "2.0.5" ring = "0.17" -secrecy.workspace = true +secrecy = { workspace = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = { version = "1.0", default-features = false } diff --git a/rust/windows-client/src-tauri/src/client/gui.rs b/rust/windows-client/src-tauri/src/client/gui.rs index 7e53afff5..5224b78cd 100755 --- a/rust/windows-client/src-tauri/src/client/gui.rs +++ b/rust/windows-client/src-tauri/src/client/gui.rs @@ -5,7 +5,10 @@ use crate::client::{self, deep_link, AppLocalDataDir}; use anyhow::{anyhow, Context, Result}; -use client::settings::{self, AdvancedSettings}; +use client::{ + logging, + settings::{self, AdvancedSettings}, +}; use connlib_client_shared::file_logger; use connlib_shared::messages::ResourceId; use secrecy::{ExposeSecret, SecretString}; @@ -79,9 +82,10 @@ pub(crate) fn run(params: client::GuiParams) -> Result<()> { } }) .invoke_handler(tauri::generate_handler![ + logging::clear_logs, + logging::export_logs, + logging::start_stop_log_counting, settings::apply_advanced_settings, - settings::clear_logs, - settings::export_logs, settings::get_advanced_settings, ]) .system_tray(tray) @@ -205,6 +209,7 @@ pub(crate) enum ControllerRequest { GetAdvancedSettings(oneshot::Sender), SchemeRequest(url::Url), SignIn, + StartStopLogCounting(bool), SignOut, UpdateResources(Vec), } @@ -395,6 +400,7 @@ async fn run_controller( .await .context("couldn't create Controller")?; + let mut log_counting_task = None; let mut resources: Vec = vec![]; tracing::debug!("GUI controller main loop start"); @@ -409,7 +415,7 @@ async fn run_controller( let mut clipboard = arboard::Clipboard::new()?; clipboard.set_text(&res.pastable)?; } - Req::ExportLogs(file_path) => settings::export_logs_to(file_path).await?, + Req::ExportLogs(file_path) => logging::export_logs_to(file_path).await?, Req::GetAdvancedSettings(tx) => { tx.send(controller.advanced_settings.clone()).ok(); } @@ -441,6 +447,19 @@ async fn run_controller( None, )?; } + Req::StartStopLogCounting(enable) => { + if enable { + if log_counting_task.is_none() { + let app = app.clone(); + log_counting_task = Some(tokio::spawn(logging::count_logs(app))); + tracing::debug!("started log counting"); + } + } else if let Some(t) = log_counting_task { + t.abort(); + log_counting_task = None; + tracing::debug!("cancelled log counting"); + } + } Req::SignOut => { keyring_entry()?.delete_password()?; if let Some(mut session) = controller.connlib_session.take() { diff --git a/rust/windows-client/src-tauri/src/client/logging.rs b/rust/windows-client/src-tauri/src/client/logging.rs index 17e7ca664..1fd43f7cc 100755 --- a/rust/windows-client/src-tauri/src/client/logging.rs +++ b/rust/windows-client/src-tauri/src/client/logging.rs @@ -1,8 +1,17 @@ -//! Separate module to contain all the `use` statements for setting up logging +//! Everything for logging to files, zipping up the files for export, and counting the files +use crate::client::gui::{ControllerRequest, CtlrTx, Managed}; use anyhow::Result; use connlib_client_shared::file_logger; -use std::{path::Path, str::FromStr}; +use serde::Serialize; +use std::{ + path::{Path, PathBuf}, + result::Result as StdResult, + str::FromStr, + time::Duration, +}; +use tauri::Manager; +use tokio::fs; use tracing::subscriber::set_global_default; use tracing_log::LogTracer; use tracing_subscriber::{fmt, layer::SubscriberExt, reload, EnvFilter, Layer, Registry}; @@ -28,3 +37,88 @@ pub(crate) fn setup(log_filter: &str) -> Result { _reloader: reloader, }) } + +#[tauri::command] +pub(crate) async fn start_stop_log_counting( + managed: tauri::State<'_, Managed>, + enable: bool, +) -> StdResult<(), String> { + managed + .ctlr_tx + .send(ControllerRequest::StartStopLogCounting(enable)) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub(crate) async fn clear_logs() -> StdResult<(), String> { + clear_logs_inner().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub(crate) async fn export_logs(managed: tauri::State<'_, Managed>) -> StdResult<(), String> { + export_logs_inner(managed.ctlr_tx.clone()) + .await + .map_err(|e| e.to_string()) +} + +pub(crate) async fn clear_logs_inner() -> Result<()> { + todo!() +} + +/// Pops up the "Save File" dialog +pub(crate) async fn export_logs_inner(ctlr_tx: CtlrTx) -> Result<()> { + let now = chrono::Local::now(); + let datetime_string = now.format("%Y_%m_%d-%H-%M"); + let filename = format!("connlib-{datetime_string}.zip"); + + tauri::api::dialog::FileDialogBuilder::new() + .add_filter("Zip", &["zip"]) + .set_file_name(&filename) + .save_file(move |file_path| match file_path { + None => {} + Some(x) => { + // blocking_send here because we're in a sync callback within Tauri somewhere + ctlr_tx + .blocking_send(ControllerRequest::ExportLogs(x)) + .unwrap() + } + }); + Ok(()) +} + +/// Exports logs to a zip file +pub(crate) async fn export_logs_to(file_path: PathBuf) -> Result<()> { + tracing::trace!("Exporting logs to {file_path:?}"); + + let mut entries = fs::read_dir("logs").await?; + while let Some(entry) = entries.next_entry().await? { + let _path = entry.path(); + // TODO: Actually add log files to a zip file + } + tokio::time::sleep(Duration::from_secs(1)).await; + // TODO: Somehow signal back to the GUI to unlock the log buttons when the export completes, or errors out + Ok(()) +} + +#[derive(Clone, Serialize)] +struct FileCount { + files: u64, + bytes: u64, +} + +pub(crate) async fn count_logs(app: tauri::AppHandle) -> Result<()> { + let mut dir = fs::read_dir("logs").await?; + let mut files: u64 = 0; + let mut bytes: u64 = 0; + while let Some(entry) = dir.next_entry().await? { + // TODO: Remove sleep before merging + // This is useful for debugging to show how the GUI thinks and make sure nothing else blocks on this + tokio::time::sleep(Duration::from_millis(200)).await; + let md = entry.metadata().await?; + files += 1; + bytes += md.len(); + app.emit_all("file_count_progress", FileCount { files, bytes })?; + } + Ok(()) +} diff --git a/rust/windows-client/src-tauri/src/client/settings.rs b/rust/windows-client/src-tauri/src/client/settings.rs index 286468733..f42693e22 100755 --- a/rust/windows-client/src-tauri/src/client/settings.rs +++ b/rust/windows-client/src-tauri/src/client/settings.rs @@ -55,18 +55,6 @@ pub(crate) async fn apply_advanced_settings( .map_err(|e| e.to_string()) } -#[tauri::command] -pub(crate) async fn clear_logs() -> StdResult<(), String> { - clear_logs_inner().await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub(crate) async fn export_logs(managed: tauri::State<'_, Managed>) -> StdResult<(), String> { - export_logs_inner(managed.ctlr_tx.clone()) - .await - .map_err(|e| e.to_string()) -} - #[tauri::command] pub(crate) async fn get_advanced_settings( managed: tauri::State<'_, Managed>, @@ -105,32 +93,3 @@ pub(crate) async fn load_advanced_settings(app: &tauri::AppHandle) -> Result Result<()> { - todo!() -} - -pub(crate) async fn export_logs_inner(ctlr_tx: gui::CtlrTx) -> Result<()> { - tauri::api::dialog::FileDialogBuilder::new() - .add_filter("Zip", &["zip"]) - .save_file(move |file_path| match file_path { - None => {} - Some(x) => ctlr_tx - .blocking_send(ControllerRequest::ExportLogs(x)) - .unwrap(), - }); - Ok(()) -} - -pub(crate) async fn export_logs_to(file_path: PathBuf) -> Result<()> { - tracing::trace!("Exporting logs to {file_path:?}"); - - let mut entries = tokio::fs::read_dir("logs").await?; - while let Some(entry) = entries.next_entry().await? { - let path = entry.path(); - tracing::trace!("Export {path:?}"); - } - tokio::time::sleep(Duration::from_secs(1)).await; - // TODO: Somehow signal back to the GUI to unlock the log buttons when the export completes, or errors out - Ok(()) -} diff --git a/rust/windows-client/src/settings.html b/rust/windows-client/src/settings.html index f038de7b7..ad26247ea 100755 --- a/rust/windows-client/src/settings.html +++ b/rust/windows-client/src/settings.html @@ -36,6 +36,9 @@ +
+

+
diff --git a/rust/windows-client/src/settings.js b/rust/windows-client/src/settings.js index b7c8b28be..e967b55f7 100755 --- a/rust/windows-client/src/settings.js +++ b/rust/windows-client/src/settings.js @@ -1,5 +1,6 @@ let auth_base_url_input; let api_url_input; +let log_count_output; let log_filter_input; let reset_advanced_settings_btn; let apply_advanced_settings_btn; @@ -11,6 +12,7 @@ const querySel = function(id) { }; const { invoke } = window.__TAURI__.tauri; +const { listen } = window.__TAURI__.event; // Lock the UI when we're saving to disk, since disk writes are technically async. // Parameters: @@ -116,12 +118,21 @@ function openTab(evt, tabName) { document.getElementById(tabName).style.display = "block"; // TODO: There's a better way to do this evt.currentTarget.className += " active"; + + invoke("start_stop_log_counting", {"enable": tabName == "tab_logs"}) + .then(() => { + // Good + }) + .catch((e) => { + console.error(e); + }); } -window.addEventListener("DOMContentLoaded", () => { +async function setup() { // Advanced tab auth_base_url_input = querySel("#auth-base-url-input"); api_url_input = querySel("#api-url-input"); + log_count_output = querySel("#log-count-output"); log_filter_input = querySel("#log-filter-input"); reset_advanced_settings_btn = querySel("#reset-advanced-settings-btn"); apply_advanced_settings_btn = querySel("#apply-advanced-settings-btn"); @@ -142,8 +153,16 @@ window.addEventListener("DOMContentLoaded", () => { clear_logs(); }); + await listen("file_count_progress", (event) => { + const pl = event.payload; + const megabytes = Math.round(pl.bytes / 100000) / 10; + log_count_output.innerText = `${pl.files} files, ${megabytes} MB`; + }); + // TODO: Why doesn't this open the Advanced tab by default? querySel("#tab_advanced").click(); get_advanced_settings().await; -}); +} + +window.addEventListener("DOMContentLoaded",setup);