Repository: GyulyVGC/sniffnet (language: rust)

Issue:
Impossible to see `stderr` and `stdout` of release builds on Windows
### Is there an existing issue for this?

- [X] I have searched the existing issues.

### What's the problem?

I set the configuration attribute [`windows-subsystem="windows"`](https://rust-lang.github.io/rfcs/1665-windows-subsystem.html#new-crate-attribute) for Windows release builds to avoid having a terminal running alongside Sniffnet when opening the app by clicking on its icon.

However, this prevents users to see `stderr` and `stdout` even when launching the app from terminal; in case of errors, this doesn't allow to see a trace of the problem, making debug more difficult.

### Additional context

A possible solution that comes to my mind is to redirect `stdout` and `stderr` to a new file in the app's configuration directory.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 77db96584..7c539abed 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -72,6 +72,9 @@ jobs:
         if: matrix.os == 'ubuntu'
         run: cargo install cross --git https://github.com/cross-rs/cross
 
+      - name: Clippy (release mode)
+        run: cargo clippy --release -- -D warnings
+
       - name: Build binary
         if: matrix.os == 'ubuntu'
         run: cross build --release --target ${{ matrix.target }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3b957d95..79dcb432e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,8 +3,9 @@
 All Sniffnet releases with the relative changes are documented in this file.
 
 ## [UNRELEASED]
-- Added Vietnamese translation 🇻🇳 ([#577](https://github.com/GyulyVGC/sniffnet/pull/577))
 - Added CLI argument `--adapter [<NAME>]` to allow immediately starting the capture from a given network interface ([#643](https://github.com/GyulyVGC/sniffnet/pull/643) — fixes [#636](https://github.com/GyulyVGC/sniffnet/issues/636))
+- Added Vietnamese translation 🇻🇳 ([#577](https://github.com/GyulyVGC/sniffnet/pull/577))
+- Redirect `stderr` and `stdout` to file on Windows release builds ([#645](https://github.com/GyulyVGC/sniffnet/pull/645) — fixes [#578](https://github.com/GyulyVGC/sniffnet/issues/578))
 - Updated some of the existing translations to v1.3: 
   - Chinese ([#575](https://github.com/GyulyVGC/sniffnet/pull/575))
   - Korean ([#604](https://github.com/GyulyVGC/sniffnet/pull/604))
diff --git a/Cargo.lock b/Cargo.lock
index 639c170e2..0dc336cb8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "ab_glyph"
@@ -1397,6 +1397,17 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "filedescriptor"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
+dependencies = [
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
 [[package]]
 name = "finl_unicode"
 version = "1.3.0"
@@ -1637,6 +1648,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "gag"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a713bee13966e9fbffdf7193af71d54a6b35a0bb34997cd6c9519ebeb5005972"
+dependencies = [
+ "filedescriptor",
+ "tempfile",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -4476,6 +4497,7 @@ dependencies = [
  "ctrlc",
  "dns-lookup",
  "etherparse",
+ "gag",
  "iced",
  "maxminddb",
  "once_cell",
diff --git a/Cargo.toml b/Cargo.toml
index 8a81abc57..bc7530428 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,6 +55,9 @@ phf_shared = "0.11.2"
 splines = "4.4.0"
 clap = { version = "4.5.21", features = ["derive"] }
 
+[target.'cfg(windows)'.dependencies]
+gag = "1.0.0"
+
 [target.'cfg(not(target_arch = "powerpc64"))'.dependencies]
 reqwest = { version = "0.12.9", default-features = false, features = ["json", "blocking", "rustls-tls"] }
 
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 6e74a36d4..f8e617a2f 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -17,20 +17,43 @@ struct Args {
     /// Start sniffing packets from the supplied network adapter
     #[arg(short, long, value_name = "NAME", default_missing_value = CONFIGS.device.device_name.as_str(), num_args = 0..=1)]
     adapter: Option<String>,
+    #[cfg(all(windows, not(debug_assertions)))]
+    /// Show the logs (stdout and stderr) of the most recent application run
+    #[arg(short, long, exclusive = true)]
+    logs: bool,
     /// Restore default settings
-    #[arg(short, long)]
+    #[arg(short, long, exclusive = true)]
     restore_default: bool,
 }
 
-pub fn parse_cli_args() -> Task<Message> {
-    let mut boot_task_chain = window::get_latest().map(Message::WindowId);
-
+pub fn handle_cli_args() -> Task<Message> {
     let args = Args::parse();
 
+    #[cfg(all(windows, not(debug_assertions)))]
+    if let Some(logs_file) = crate::utils::formatted_strings::get_logs_file_path() {
+        if args.logs {
+            std::process::Command::new("explorer")
+                .arg(logs_file)
+                .spawn()
+                .unwrap()
+                .wait()
+                .unwrap_or_default();
+            std::process::exit(0);
+        } else {
+            // truncate logs file
+            let _ = std::fs::OpenOptions::new()
+                .write(true)
+                .truncate(true)
+                .open(logs_file);
+        }
+    }
+
     if args.restore_default {
         Configs::default().store();
+        std::process::exit(0);
     }
 
+    let mut boot_task_chain = window::get_latest().map(Message::WindowId);
     if let Some(adapter) = args.adapter {
         boot_task_chain = boot_task_chain
             .chain(Task::done(Message::AdapterSelection(adapter)))
diff --git a/src/main.rs b/src/main.rs
index 9811672df..4171dd12c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,7 +12,7 @@ use iced::{application, window, Font, Pixels, Settings};
 
 use chart::types::chart_type::ChartType;
 use chart::types::traffic_chart::TrafficChart;
-use cli::parse_cli_args;
+use cli::handle_cli_args;
 use configs::types::config_device::ConfigDevice;
 use configs::types::config_settings::ConfigSettings;
 use gui::pages::types::running_page::RunningPage;
@@ -55,9 +55,18 @@ pub const SNIFFNET_TITLECASE: &str = "Sniffnet";
 ///
 /// It initializes shared variables and loads configuration parameters
 pub fn main() -> iced::Result {
-    let configs = CONFIGS.clone();
+    #[cfg(all(windows, not(debug_assertions)))]
+    let _gag1: gag::Redirect<std::fs::File>;
+    #[cfg(all(windows, not(debug_assertions)))]
+    let _gag2: gag::Redirect<std::fs::File>;
+    #[cfg(all(windows, not(debug_assertions)))]
+    if let Some((gag1, gag2)) = utils::formatted_strings::redirect_stdout_stderr_to_file() {
+        _gag1 = gag1;
+        _gag2 = gag2;
+    }
 
-    let boot_task_chain = parse_cli_args();
+    let configs = CONFIGS.clone();
+    let boot_task_chain = handle_cli_args();
 
     let configs1 = Arc::new(Mutex::new(configs));
     let configs2 = configs1.clone();
diff --git a/src/utils/formatted_strings.rs b/src/utils/formatted_strings.rs
index a022b7662..3af31d961 100644
--- a/src/utils/formatted_strings.rs
+++ b/src/utils/formatted_strings.rs
@@ -165,6 +165,31 @@ pub fn get_formatted_num_seconds(num_seconds: u128) -> String {
     }
 }
 
+#[allow(dead_code)]
+#[cfg(windows)]
+pub fn get_logs_file_path() -> Option<String> {
+    let mut conf = confy::get_configuration_file_path(crate::SNIFFNET_LOWERCASE, "logs").ok()?;
+    conf.set_extension("txt");
+    Some(conf.to_str()?.to_string())
+}
+
+#[cfg(all(windows, not(debug_assertions)))]
+pub fn redirect_stdout_stderr_to_file(
+) -> Option<(gag::Redirect<std::fs::File>, gag::Redirect<std::fs::File>)> {
+    if let Ok(logs_file) = std::fs::OpenOptions::new()
+        .write(true)
+        .create(true)
+        .append(true)
+        .open(get_logs_file_path()?)
+    {
+        return Some((
+            gag::Redirect::stdout(logs_file.try_clone().ok()?).ok()?,
+            gag::Redirect::stderr(logs_file).ok()?,
+        ));
+    }
+    None
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -200,4 +225,12 @@ mod tests {
             "94522879700260684295381835397713392:04:15"
         );
     }
+
+    #[cfg(windows)]
+    #[test]
+    fn test_logs_file_path() {
+        let file_path = std::path::PathBuf::from(get_logs_file_path().unwrap());
+        assert!(file_path.is_absolute());
+        assert_eq!(file_path.file_name().unwrap(), "logs.txt");
+    }
 }


Test patch: