usbwatch-rs 0.4.5

A cross-platform USB device monitoring tool written in Rust
Documentation
//! # USBWatch CLI
//!
//! This is the command-line interface for USBWatch.
//!
//! ## Subcommands
//! - `monitor` (default): Monitor USB device events in real-time
//! - `install`: Install usbwatch to system PATH
//! - `uninstall`: Uninstall usbwatch from system PATH
//!
//! ## Options
//! - `--json`: Output events in JSON format
//! - `--logfile <PATH>`: Log events to the specified file
//!
//! For installation and troubleshooting, see INSTALL.md.
mod device_info;
mod logger;
mod watcher;

use clap::{Parser, Subcommand};
use logger::{logger_task, Logger};
use std::env;
use std::fs;
use std::path::Path;
use tokio::sync::mpsc;
use watcher::UsbWatcher;

#[derive(Parser)]
#[command(name = "usbwatch")]
#[command(about = "A cross-platform USB device monitor")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(long_about = format!("USBWatch v{}\nA cross-platform USB device monitoring tool\n\nRepository: {}", 
    env!("CARGO_PKG_VERSION"), 
    env!("CARGO_PKG_REPOSITORY")))]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Output events in JSON format (monitor mode only)
    #[arg(long, global = true)]
    json: bool,

    /// Log events to file (monitor mode only)
    #[arg(long, value_name = "PATH", global = true)]
    logfile: Option<String>,
}

#[derive(Subcommand)]
enum Commands {
    /// Monitor USB device events (default)
    Monitor,
    /// Install usbwatch to system PATH
    Install,
    /// Uninstall usbwatch from system PATH
    Uninstall,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    match cli.command.unwrap_or(Commands::Monitor) {
        Commands::Monitor => run_monitor(cli.json, cli.logfile).await,
        Commands::Install => install_binary(),
        Commands::Uninstall => uninstall_binary(),
    }
}

async fn run_monitor(
    json: bool,
    logfile: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    println!(
        "๐Ÿ”Œ USB Device Monitor - usbwatch v{}",
        env!("CARGO_PKG_VERSION")
    );
    println!("Press Ctrl+C to stop monitoring...");

    // Create channel for device events
    let (tx, rx) = mpsc::channel(100);

    // Detect if terminal supports colour
    let colourful = atty::is(atty::Stream::Stdout);
    // Initialise logger
    let logger = Logger::new(json, logfile.as_deref(), colourful)?;

    // Start logger task
    let logger_handle = tokio::spawn(logger_task(rx, logger));

    // Create and start USB watcher
    let watcher = UsbWatcher::new(tx)?;

    // Handle Ctrl+C gracefully
    let watcher_handle = tokio::spawn(async move {
        if let Err(e) = watcher.start_monitoring().await {
            eprintln!("USB monitoring error: {e}");
        }
    });

    // Wait for Ctrl+C
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            println!("\n๐Ÿ“ก Shutting down USB monitor...");
        }
        _ = watcher_handle => {
            println!("๐Ÿ“ก USB monitoring stopped");
        }
    }

    // Cleanup
    logger_handle.abort();

    Ok(())
}

fn install_binary() -> Result<(), Box<dyn std::error::Error>> {
    let current_exe = env::current_exe()?;
    let exe_name = if cfg!(windows) {
        "usbwatch.exe"
    } else {
        "usbwatch"
    };

    // Determine target directory
    let target_dir = if cfg!(windows) {
        // On Windows, try to install to a directory in PATH
        let program_files =
            env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
        Path::new(&program_files).join("usbwatch")
    } else {
        // On Unix-like systems, install to /usr/local/bin
        Path::new("/usr/local/bin").to_path_buf()
    };

    let target_path = target_dir.join(exe_name);

    // Create target directory if it doesn't exist (Windows only)
    if cfg!(windows) {
        fs::create_dir_all(&target_dir)?;
    }

    // Copy the binary
    fs::copy(&current_exe, &target_path)?;

    // Set executable permissions on Unix
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&target_path)?.permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&target_path, perms)?;
    }

    println!(
        "โœ… Successfully installed usbwatch to {}",
        target_path.display()
    );

    if cfg!(windows) {
        println!(
            "๐Ÿ“ Note: You may need to add {} to your PATH environment variable",
            target_dir.display()
        );
    }

    println!("๐Ÿš€ You can now run 'usbwatch' from anywhere!");

    Ok(())
}

fn uninstall_binary() -> Result<(), Box<dyn std::error::Error>> {
    let exe_name = if cfg!(windows) {
        "usbwatch.exe"
    } else {
        "usbwatch"
    };

    // Determine target directory
    let target_dir = if cfg!(windows) {
        let program_files =
            env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
        Path::new(&program_files).join("usbwatch")
    } else {
        Path::new("/usr/local/bin").to_path_buf()
    };

    let target_path = target_dir.join(exe_name);

    if target_path.exists() {
        fs::remove_file(&target_path)?;
        println!(
            "โœ… Successfully uninstalled usbwatch from {}",
            target_path.display()
        );

        // Remove directory on Windows if empty
        if cfg!(windows) && target_dir.read_dir()?.next().is_none() {
            fs::remove_dir(&target_dir)?;
            println!("๐Ÿ—‘๏ธ  Removed empty directory {}", target_dir.display());
        }
    } else {
        println!("โŒ usbwatch is not installed at {}", target_path.display());
        return Ok(());
    }

    println!("๐Ÿงน usbwatch has been uninstalled");

    Ok(())
}