The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\main.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     2→//!
     3→//! This agent connects to the GuruRMM server, reports system metrics,
     4→//! monitors services (watchdog), and executes remote commands.
     5→
     6→mod config;
     7→mod device_id;
     8→mod ipc;
     9→mod metrics;
    10→mod service;
    11→mod transport;
    12→mod tray_launcher;
    13→mod updater;
    14→
    15→use anyhow::{Context, Result};
    16→use clap::{Parser, Subcommand};
    17→use std::path::PathBuf;

mod config;
mod device_id;
mod ipc;
mod metrics;
mod service;
mod transport;
mod tray_launcher;
mod updater;

//! GuruRMM Agent - Cross-platform Remote Monitoring and Management Agent
//!
//! This agent connects to the GuruRMM server, reports system metrics,
//! monitors services (watchdog), and executes remote commands.

mod config;
mod device_id;
mod ipc;
mod metrics;
mod service;
mod transport;
mod updater;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};

use crate::config::AgentConfig;
use crate::metrics::MetricsCollector;
use crate::transport::WebSocketClient;

/// GuruRMM Agent - Remote Monitoring and Management
#[derive(Parser)]
#[command(name = "gururmm-agent")]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Path to configuration file
    #[arg(short, long, default_value = "agent.toml")]
    config: PathBuf,

    /// Subcommand to run
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Run the agent (default)
    Run,

    /// Install as a system service
    Install {
        /// Server WebSocket URL (e.g., wss://rmm-api.example.com/ws)
        #[arg(long)]
        server_url: Option<String>,

        /// API key for authentication
        #[arg(long)]
        api_key: Option<String>,

        /// Skip legacy service detection and cleanup
        #[arg(long, default_value = "false")]
        skip_legacy_check: bool,
    },

    /// Uninstall the system service
    Uninstall,

    /// Start the installed service
    Start,

    /// Stop the installed service
    Stop,

    /// Show agent status
    Status,

    /// Generate a sample configuration file
    GenerateConfig {
        /// Output path for config file
        #[arg(short, long, default_value = "agent.toml")]
        output: PathBuf,
    },

    /// Run as Windows service (called by SCM, not for manual use)
    #[command(hide = true)]
    Service,
}

/// Shared application state
pub struct AppState {
    pub config: AgentConfig,
    pub metrics_collector: MetricsCollector,
    pub connected: RwLock<bool>,
    pub last_checkin: RwLock<Option<chrono::DateTime<chrono::Utc>>>,
    pub tray_policy: RwLock<ipc::TrayPolicy>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("gururmm_agent=info".parse()?)
                .add_directive("info".parse()?),
        )
        .init();

    let cli = Cli::parse();

    match cli.command.unwrap_or(Commands::Run) {
        Commands::Run => run_agent(cli.config).await,
        Commands::Install { server_url, api_key, skip_legacy_check } => {
            install_service(server_url, api_key, skip_legacy_check).await
        }
        Commands::Uninstall => uninstall_service().await,
        Commands::Start => start_service().await,
        Commands::Stop => stop_service().await,
        Commands::Status => show_status(cli.config).await,
        Commands::GenerateConfig { output } => generate_config(output).await,
        Commands::Service => run_as_windows_service(),
    }
}

/// Run as a Windows service (called by SCM)
fn run_as_windows_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::run_as_service()
    }

    #[cfg(not(windows))]
    {
        anyhow::bail!("Windows service mode is only available on Windows");
    }
}

/// Main agent runtime loop
async fn run_agent(config_path: PathBuf) -> Result<()> {
    info!("GuruRMM Agent starting...");

    // Load configuration
    let config = AgentConfig::load(&config_path)?;
    info!("Loaded configuration from {:?}", config_path);
    info!("Server URL: {}", config.server.url);

    // Initialize metrics collector
    let metrics_collector = MetricsCollector::new();
    info!("Metrics collector initialized");

    // Create channels for IPC commands
    let (force_checkin_tx, mut force_checkin_rx) = tokio::sync::mpsc::channel::<()>(8);
    let (stop_agent_tx, mut stop_agent_rx) = tokio::sync::mpsc::channel::<()>(1);

    // Create shared state
    let state = Arc::new(AppState {
        config: config.clone(),
        metrics_collector,
        connected: RwLock::new(false),
        last_checkin: RwLock::new(None),
        tray_policy: RwLock::new(ipc::TrayPolicy::default_permissive()),
    });

    // Create IPC state for tray communication
    let device_id = device_id::get_device_id();
    let hostname = hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    let initial_status = ipc::AgentStatus {
        connected: false,
        last_checkin: None,
        server_url: config.server.url.clone(),
        agent_version: env!("CARGO_PKG_VERSION").to_string(),
        device_id,
        hostname,
        error: None,
    };

    let ipc_state = Arc::new(ipc::IpcState::new(
        initial_status,
        ipc::TrayPolicy::default_permissive(),
        force_checkin_tx,
        stop_agent_tx,
    ));

    // Start IPC server for tray application
    let ipc_server_state = Arc::clone(&ipc_state);
    let ipc_handle = tokio::spawn(async move {
        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
            error!("IPC server error: {}", e);
        }
    });
    info!("IPC server started");

    // Start the WebSocket client with auto-reconnect
    let ws_state = Arc::clone(&state);
    let ws_handle = tokio::spawn(async move {
        loop {
            info!("Connecting to server...");
            match WebSocketClient::connect_and_run(Arc::clone(&ws_state)).await {
                Ok(_) => {
                    warn!("WebSocket connection closed normally, reconnecting...");
                }
                Err(e) => {
                    error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
                }
            }

            // Mark as disconnected
            *ws_state.connected.write().await = false;

            // Wait before reconnecting
            tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
        }
    });

    // Start metrics collection loop
    let metrics_state = Arc::clone(&state);
    let metrics_handle = tokio::spawn(async move {
        let interval = metrics_state.config.metrics.interval_seconds;
        let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval));

        loop {
            interval_timer.tick().await;

            // Collect metrics (they'll be sent via WebSocket if connected)
            let metrics = metrics_state.metrics_collector.collect().await;
            if *metrics_state.connected.read().await {
                info!(
                    "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
                    metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
                );
            }
        }
    });

    // Task to update IPC status periodically
    let ipc_update_state = Arc::clone(&state);
    let ipc_update_ipc = Arc::clone(&ipc_state);
    let ipc_update_handle = tokio::spawn(async move {
        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
        loop {
            interval.tick().await;

            let connected = *ipc_update_state.connected.read().await;
            let last_checkin = ipc_update_state.last_checkin.read().await.map(|dt| dt.to_rfc3339());

            let status = ipc::AgentStatus {
                connected,
                last_checkin,
                server_url: ipc_update_state.config.server.url.clone(),
                agent_version: env!("CARGO_PKG_VERSION").to_string(),
                device_id: device_id::get_device_id(),
                hostname: hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string()),
                error: if connected { None } else { Some("Disconnected".to_string()) },
            };

            ipc_update_ipc.update_status(status).await;
        }
    });

    // Wait for shutdown signal or stop request
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received shutdown signal");
        }
        _ = stop_agent_rx.recv() => {
            info!("Received stop request from tray");
        }
        _ = ws_handle => {
            error!("WebSocket task ended unexpectedly");
        }
        _ = metrics_handle => {
            error!("Metrics task ended unexpectedly");
        }
        _ = ipc_handle => {
            error!("IPC server ended unexpectedly");
        }
        _ = ipc_update_handle => {
            error!("IPC update task ended unexpectedly");
        }
    }

    info!("GuruRMM Agent shutting down");
    Ok(())
}

/// Install the agent as a system service
async fn install_service(
    server_url: Option<String>,
    api_key: Option<String>,
    skip_legacy_check: bool,
) -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::install(server_url, api_key, skip_legacy_check)
    }

    #[cfg(target_os = "linux")]
    {
        install_systemd_service(server_url, api_key, skip_legacy_check).await
    }

    #[cfg(target_os = "macos")]
    {
        let _ = (server_url, api_key, skip_legacy_check); // Suppress unused warnings
        info!("Installing GuruRMM Agent as launchd service...");
        todo!("macOS launchd service installation not yet implemented");
    }
}

/// Legacy service names to check for and clean up (Linux)
#[cfg(target_os = "linux")]
const LINUX_LEGACY_SERVICE_NAMES: &[&str] = &[
    "gururmm",           // Old name without -agent suffix
    "guru-rmm-agent",    // Alternative naming
    "GuruRMM-Agent",     // Case variant
];

/// Clean up legacy Linux service installations
#[cfg(target_os = "linux")]
fn cleanup_legacy_linux_services() -> Result<()> {
    use std::process::Command;

    info!("Checking for legacy service installations...");

    for legacy_name in LINUX_LEGACY_SERVICE_NAMES {
        // Check if service exists
        let status = Command::new("systemctl")
            .args(["status", legacy_name])
            .output();

        if let Ok(output) = status {
            if output.status.success() || String::from_utf8_lossy(&output.stderr).contains("Loaded:") {
                info!("Found legacy service '{}', removing...", legacy_name);

                // Stop the service
                let _ = Command::new("systemctl")
                    .args(["stop", legacy_name])
                    .status();

                // Disable the service
                let _ = Command::new("systemctl")
                    .args(["disable", legacy_name])
                    .status();

                // Remove unit file
                let unit_file = format!("/etc/systemd/system/{}.service", legacy_name);
                if std::path::Path::new(&unit_file).exists() {
                    info!("Removing legacy unit file: {}", unit_file);
                    let _ = std::fs::remove_file(&unit_file);
                }
            }
        }
    }

    // Check for legacy binaries in common locations
    let legacy_binary_locations = [
        "/usr/local/bin/gururmm",
        "/usr/bin/gururmm",
        "/opt/gururmm/gururmm",
        "/opt/gururmm/agent",
    ];

    for legacy_path in legacy_binary_locations {
        if std::path::Path::new(legacy_path).exists() {
            info!("Found legacy binary at '{}', removing...", legacy_path);
            let _ = std::fs::remove_file(legacy_path);
        }
    }

    // Reload systemd to pick up removed unit files
    let _ = Command::new("systemctl")
        .args(["daemon-reload"])
        .status();

    Ok(())
}

/// Install as a systemd service (Linux)
#[cfg(target_os = "linux")]
async fn install_systemd_service(
    server_url: Option<String>,
    api_key: Option<String>,
    skip_legacy_check: bool,
) -> Result<()> {
    use std::process::Command;

    const SERVICE_NAME: &str = "gururmm-agent";
    const INSTALL_DIR: &str = "/usr/local/bin";
    const CONFIG_DIR: &str = "/etc/gururmm";
    const SYSTEMD_DIR: &str = "/etc/systemd/system";

    info!("Installing GuruRMM Agent as systemd service...");

    // Check if running as root
    if !nix::unistd::geteuid().is_root() {
        anyhow::bail!("Installation requires root privileges. Please run with sudo.");
    }

    // Clean up legacy installations unless skipped
    if !skip_legacy_check {
        if let Err(e) = cleanup_legacy_linux_services() {
            warn!("Legacy cleanup warning: {}", e);
        }
    }

    // Get the current executable path
    let current_exe = std::env::current_exe()
        .context("Failed to get current executable path")?;

    let binary_dest = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
    let config_dest = format!("{}/agent.toml", CONFIG_DIR);
    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);

    // Create config directory
    info!("Creating config directory: {}", CONFIG_DIR);
    std::fs::create_dir_all(CONFIG_DIR)
        .context("Failed to create config directory")?;

    // Copy binary
    info!("Copying binary to: {}", binary_dest);
    std::fs::copy(&current_exe, &binary_dest)
        .context("Failed to copy binary")?;

    // Make binary executable
    Command::new("chmod")
        .args(["+x", &binary_dest])
        .status()
        .context("Failed to set binary permissions")?;

    // Handle configuration
    let config_needs_manual_edit;
    if !std::path::Path::new(&config_dest).exists() {
        info!("Creating config: {}", config_dest);

        // Start with sample config
        let mut config = crate::config::AgentConfig::sample();

        // Apply provided values
        if let Some(url) = &server_url {
            config.server.url = url.clone();
        }
        if let Some(key) = &api_key {
            config.server.api_key = key.clone();
        }

        let toml_str = toml::to_string_pretty(&config)?;
        std::fs::write(&config_dest, toml_str)
            .context("Failed to write config file")?;

        // Set restrictive permissions on config (contains API key)
        Command::new("chmod")
            .args(["600", &config_dest])
            .status()
            .context("Failed to set config permissions")?;

        config_needs_manual_edit = server_url.is_none() || api_key.is_none();
    } else {
        info!("Config already exists: {}", config_dest);
        config_needs_manual_edit = false;

        // If server_url or api_key provided, update existing config
        if server_url.is_some() || api_key.is_some() {
            info!("Updating existing configuration...");
            let config_content = std::fs::read_to_string(&config_dest)?;
            let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
                .context("Failed to parse existing config")?;

            if let Some(url) = &server_url {
                config.server.url = url.clone();
            }
            if let Some(key) = &api_key {
                config.server.api_key = key.clone();
            }

            let toml_str = toml::to_string_pretty(&config)?;
            std::fs::write(&config_dest, toml_str)
                .context("Failed to update config file")?;
        }
    }

    // Create systemd unit file
    let unit_content = format!(r#"[Unit]
Description=GuruRMM Agent - Remote Monitoring and Management
Documentation=https://github.com/azcomputerguru/gururmm
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart={binary} --config {config} run
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier={service}

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
ReadWritePaths=/var/log

[Install]
WantedBy=multi-user.target
"#,
        binary = binary_dest,
        config = config_dest,
        service = SERVICE_NAME
    );

    info!("Creating systemd unit file: {}", unit_file);
    std::fs::write(&unit_file, unit_content)
        .context("Failed to write systemd unit file")?;

    // Reload systemd daemon
    info!("Reloading systemd daemon...");
    let status = Command::new("systemctl")
        .args(["daemon-reload"])
        .status()
        .context("Failed to reload systemd")?;

    if !status.success() {
        anyhow::bail!("systemctl daemon-reload failed");
    }

    // Enable the service
    info!("Enabling service...");
    let status = Command::new("systemctl")
        .args(["enable", SERVICE_NAME])
        .status()
        .context("Failed to enable service")?;

    if !status.success() {
        anyhow::bail!("systemctl enable failed");
    }

    println!("\n✓ GuruRMM Agent installed successfully!");
    println!("\nInstalled files:");
    println!("  Binary: {}", binary_dest);
    println!("  Config: {}", config_dest);
    println!("  Service: {}", unit_file);

    if config_needs_manual_edit {
        println!("\n⚠  IMPORTANT: Edit {} with your server URL and API key!", config_dest);
        println!("\nNext steps:");
        println!("  1. Edit {} with your server URL and API key", config_dest);
        println!("  2. Start the service: sudo systemctl start {}", SERVICE_NAME);
    } else {
        println!("\nStarting service...");
        let status = Command::new("systemctl")
            .args(["start", SERVICE_NAME])
            .status();

        if status.is_ok() && status.unwrap().success() {
            println!("✓ Service started successfully!");
        } else {
            println!("⚠  Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
        }
    }

    println!("\nUseful commands:");
    println!("  Status: sudo systemctl status {}", SERVICE_NAME);
    println!("  Logs:   sudo journalctl -u {} -f", SERVICE_NAME);
    println!("  Stop:   sudo systemctl stop {}", SERVICE_NAME);
    println!("  Start:  sudo systemctl start {}", SERVICE_NAME);

    Ok(())
}

/// Uninstall the system service
async fn uninstall_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::uninstall()
    }

    #[cfg(target_os = "linux")]
    {
        uninstall_systemd_service().await
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service uninstallation not yet implemented");
    }
}

/// Uninstall systemd service (Linux)
#[cfg(target_os = "linux")]
async fn uninstall_systemd_service() -> Result<()> {
    use std::process::Command;

    const SERVICE_NAME: &str = "gururmm-agent";
    const INSTALL_DIR: &str = "/usr/local/bin";
    const CONFIG_DIR: &str = "/etc/gururmm";
    const SYSTEMD_DIR: &str = "/etc/systemd/system";

    info!("Uninstalling GuruRMM Agent...");

    if !nix::unistd::geteuid().is_root() {
        anyhow::bail!("Uninstallation requires root privileges. Please run with sudo.");
    }

    let binary_path = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);

    // Stop the service if running
    info!("Stopping service...");
    let _ = Command::new("systemctl")
        .args(["stop", SERVICE_NAME])
        .status();

    // Disable the service
    info!("Disabling service...");
    let _ = Command::new("systemctl")
        .args(["disable", SERVICE_NAME])
        .status();

    // Remove unit file
    if std::path::Path::new(&unit_file).exists() {
        info!("Removing unit file: {}", unit_file);
        std::fs::remove_file(&unit_file)?;
    }

    // Remove binary
    if std::path::Path::new(&binary_path).exists() {
        info!("Removing binary: {}", binary_path);
        std::fs::remove_file(&binary_path)?;
    }

    // Reload systemd
    let _ = Command::new("systemctl")
        .args(["daemon-reload"])
        .status();

    println!("\n✓ GuruRMM Agent uninstalled successfully!");
    println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
    println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);

    Ok(())
}

/// Start the installed service
async fn start_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::start()
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::Command;

        info!("Starting GuruRMM Agent service...");

        let status = Command::new("systemctl")
            .args(["start", "gururmm-agent"])
            .status()
            .context("Failed to start service")?;

        if status.success() {
            println!("** Service started successfully");
            println!("Check status: sudo systemctl status gururmm-agent");
        } else {
            anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service start not yet implemented");
    }
}

/// Stop the installed service
async fn stop_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::stop()
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::Command;

        info!("Stopping GuruRMM Agent service...");

        let status = Command::new("systemctl")
            .args(["stop", "gururmm-agent"])
            .status()
            .context("Failed to stop service")?;

        if status.success() {
            println!("** Service stopped successfully");
        } else {
            anyhow::bail!("Failed to stop service");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service stop not yet implemented");
    }
}

/// Show agent status
async fn show_status(config_path: PathBuf) -> Result<()> {
    // On Windows, show service status
    #[cfg(windows)]
    {
        service::windows::status()?;
        println!();
    }

    // Try to load config for additional info
    match AgentConfig::load(&config_path) {
        Ok(config) => {
            println!("Configuration");
            println!("=============");
            println!("Config file: {:?}", config_path);
            println!("Server URL: {}", config.server.url);
            println!("Metrics interval: {} seconds", config.metrics.interval_seconds);
            println!("Watchdog enabled: {}", config.watchdog.enabled);

            // Collect current metrics
            let collector = MetricsCollector::new();
            let metrics = collector.collect().await;

            println!("\nCurrent System Metrics:");
            println!("  CPU Usage: {:.1}%", metrics.cpu_percent);
            println!("  Memory Usage: {:.1}%", metrics.memory_percent);
            println!(
                "  Memory Used: {:.2} GB",
                metrics.memory_used_bytes as f64 / 1_073_741_824.0
            );
            println!("  Disk Usage: {:.1}%", metrics.disk_percent);
            println!(
                "  Disk Used: {:.2} GB",
                metrics.disk_used_bytes as f64 / 1_073_741_824.0
            );
        }
        Err(_) => {
            println!("\nConfig file {:?} not found or invalid.", config_path);
            #[cfg(windows)]
            println!("Service config location: {}\\agent.toml", service::windows::CONFIG_DIR);
        }
    }

    Ok(())
}

/// Generate a sample configuration file
async fn generate_config(output: PathBuf) -> Result<()> {
    let sample_config = AgentConfig::sample();
    let toml_str = toml::to_string_pretty(&sample_config)?;

    std::fs::write(&output, toml_str)?;
    println!("Sample configuration written to {:?}", output);
    println!("\nEdit this file with your server URL and API key, then run:");
    println!("  gururmm-agent --config {:?} run", output);

    Ok(())
}

mod metrics;
mod service;
mod transport;
+mod tray_launcher;
mod updater;
use anyhow::{Context, Result};


Now I need to add the tray launcher integration into the `run_agent` function. I'll add it after the IPC server starts.


Edit

    // Start IPC server for tray application
    let ipc_server_state = Arc::clone(&ipc_state);
    let ipc_handle = tokio::spawn(async move {
        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
            error!("IPC server error: {}", e);
        }
    });
    info!("IPC server started");

    // Start the WebSocket client with auto-reconnect

    // Start IPC server for tray application
    let ipc_server_state = Arc::clone(&ipc_state);
    let ipc_handle = tokio::spawn(async move {
        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
            error!("IPC server error: {}", e);
        }
    });
    info!("IPC server started");

    // Start tray launcher (Windows only for now)
    #[cfg(windows)]
    let tray_launcher = {
        let tray_exe_path = std::path::PathBuf::from(
            format!("{}\\gururmm-tray.exe", service::windows::INSTALL_DIR)
        );
        let launcher = Arc::new(tray_launcher::TrayLauncher::new(tray_exe_path));

        // Launch tray in existing sessions
        let launcher_init = Arc::clone(&launcher);
        tokio::spawn(async move {
            // Small delay to let IPC server fully start
            tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
            launcher_init.ensure_running().await;
        });

        // Periodic check for new sessions (every 30 seconds)
        let launcher_periodic = Arc::clone(&launcher);
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
            loop {
                interval.tick().await;
                launcher_periodic.ensure_running().await;
            }
        });

        info!("Tray launcher initialized");
        Some(launcher)
    };

    #[cfg(not(windows))]
    let tray_launcher: Option<Arc<tray_launcher::TrayLauncher>> = None;

    // Start the WebSocket client with auto-reconnect


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\main.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   178→        force_checkin_tx,
   179→        stop_agent_tx,
   180→    ));
   181→
   182→    // Start IPC server for tray application
   183→    let ipc_server_state = Arc::clone(&ipc_state);
   184→    let ipc_handle = tokio::spawn(async move {
   185→        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
   186→            error!("IPC server error: {}", e);
   187→        }
   188→    });
   189→    info!("IPC server started");
   190→
   191→    // Start tray launcher (Windows only for now)
   192→    #[cfg(windows)]
   193→    let tray_launcher = {
   194→        let tray_exe_path = std::path::PathBuf::from(
   195→            format!("{}\\gururmm-tray.exe", service::windows::INSTALL_DIR)
   196→        );
   197→        let launcher = Arc::new(tray_launcher::TrayLauncher::new(tray_exe_path));
   198→
   199→        // Launch tray in existing sessions
   200→        let launcher_init = Arc::clone(&launcher);
   201→        tokio::spawn(async move {
   202→            // Small delay to let IPC server fully start
   203→            tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
   204→            launcher_init.ensure_running().await;
   205→        });
   206→
   207→        // Periodic check for new sessions (every 30 seconds)
   208→        let launcher_periodic = Arc::clone(&launcher);
   209→        tokio::spawn(async move {
   210→            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
   211→            loop {
   212→                interval.tick().await;
   213→                launcher_periodic.ensure_running().await;
   214→            }
   215→        });
   216→
   217→        info!("Tray launcher initialized");
   218→        Some(launcher)
   219→    };
   220→
   221→    #[cfg(not(windows))]
   222→    let tray_launcher: Option<Arc<tray_launcher::TrayLauncher>> = None;
   223→
   224→    // Start the WebSocket client with auto-reconnect
   225→    let ws_state = Arc::clone(&state);
   226→    let ws_handle = tokio::spawn(async move {
   227→        loop {
   228→            info!("Connecting to server...");

    // Start IPC server for tray application
    let ipc_server_state = Arc::clone(&ipc_state);
    let ipc_handle = tokio::spawn(async move {
        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
            error!("IPC server error: {}", e);
        }
    });
    info!("IPC server started");

    // Start tray launcher (Windows only for now)
    #[cfg(windows)]
    let tray_launcher = {
        let tray_exe_path = std::path::PathBuf::from(
            format!("{}\\gururmm-tray.exe", service::windows::INSTALL_DIR)
        );
        let launcher = Arc::new(tray_launcher::TrayLauncher::new(tray_exe_path));

        // Launch tray in existing sessions
        let launcher_init = Arc::clone(&launcher);
        tokio::spawn(async move {
            // Small delay to let IPC server fully start
            tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
            launcher_init.ensure_running().await;
        });

        // Periodic check for new sessions (every 30 seconds)
        let launcher_periodic = Arc::clone(&launcher);
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
            loop {
                interval.tick().await;
                launcher_periodic.ensure_running().await;
            }
        });

        info!("Tray launcher initialized");
        Some(launcher)
    };

    #[cfg(not(windows))]
    let tray_launcher: Option<Arc<tray_launcher::TrayLauncher>> = None;

    // Start the WebSocket client with auto-reconnect

//! GuruRMM Agent - Cross-platform Remote Monitoring and Management Agent
//!
//! This agent connects to the GuruRMM server, reports system metrics,
//! monitors services (watchdog), and executes remote commands.

mod config;
mod device_id;
mod ipc;
mod metrics;
mod service;
mod transport;
mod tray_launcher;
mod updater;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};

use crate::config::AgentConfig;
use crate::metrics::MetricsCollector;
use crate::transport::WebSocketClient;

/// GuruRMM Agent - Remote Monitoring and Management
#[derive(Parser)]
#[command(name = "gururmm-agent")]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Path to configuration file
    #[arg(short, long, default_value = "agent.toml")]
    config: PathBuf,

    /// Subcommand to run
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Run the agent (default)
    Run,

    /// Install as a system service
    Install {
        /// Server WebSocket URL (e.g., wss://rmm-api.example.com/ws)
        #[arg(long)]
        server_url: Option<String>,

        /// API key for authentication
        #[arg(long)]
        api_key: Option<String>,

        /// Skip legacy service detection and cleanup
        #[arg(long, default_value = "false")]
        skip_legacy_check: bool,
    },

    /// Uninstall the system service
    Uninstall,

    /// Start the installed service
    Start,

    /// Stop the installed service
    Stop,

    /// Show agent status
    Status,

    /// Generate a sample configuration file
    GenerateConfig {
        /// Output path for config file
        #[arg(short, long, default_value = "agent.toml")]
        output: PathBuf,
    },

    /// Run as Windows service (called by SCM, not for manual use)
    #[command(hide = true)]
    Service,
}

/// Shared application state
pub struct AppState {
    pub config: AgentConfig,
    pub metrics_collector: MetricsCollector,
    pub connected: RwLock<bool>,
    pub last_checkin: RwLock<Option<chrono::DateTime<chrono::Utc>>>,
    pub tray_policy: RwLock<ipc::TrayPolicy>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("gururmm_agent=info".parse()?)
                .add_directive("info".parse()?),
        )
        .init();

    let cli = Cli::parse();

    match cli.command.unwrap_or(Commands::Run) {
        Commands::Run => run_agent(cli.config).await,
        Commands::Install { server_url, api_key, skip_legacy_check } => {
            install_service(server_url, api_key, skip_legacy_check).await
        }
        Commands::Uninstall => uninstall_service().await,
        Commands::Start => start_service().await,
        Commands::Stop => stop_service().await,
        Commands::Status => show_status(cli.config).await,
        Commands::GenerateConfig { output } => generate_config(output).await,
        Commands::Service => run_as_windows_service(),
    }
}

/// Run as a Windows service (called by SCM)
fn run_as_windows_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::run_as_service()
    }

    #[cfg(not(windows))]
    {
        anyhow::bail!("Windows service mode is only available on Windows");
    }
}

/// Main agent runtime loop
async fn run_agent(config_path: PathBuf) -> Result<()> {
    info!("GuruRMM Agent starting...");

    // Load configuration
    let config = AgentConfig::load(&config_path)?;
    info!("Loaded configuration from {:?}", config_path);
    info!("Server URL: {}", config.server.url);

    // Initialize metrics collector
    let metrics_collector = MetricsCollector::new();
    info!("Metrics collector initialized");

    // Create channels for IPC commands
    let (force_checkin_tx, mut force_checkin_rx) = tokio::sync::mpsc::channel::<()>(8);
    let (stop_agent_tx, mut stop_agent_rx) = tokio::sync::mpsc::channel::<()>(1);

    // Create shared state
    let state = Arc::new(AppState {
        config: config.clone(),
        metrics_collector,
        connected: RwLock::new(false),
        last_checkin: RwLock::new(None),
        tray_policy: RwLock::new(ipc::TrayPolicy::default_permissive()),
    });

    // Create IPC state for tray communication
    let device_id = device_id::get_device_id();
    let hostname = hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    let initial_status = ipc::AgentStatus {
        connected: false,
        last_checkin: None,
        server_url: config.server.url.clone(),
        agent_version: env!("CARGO_PKG_VERSION").to_string(),
        device_id,
        hostname,
        error: None,
    };

    let ipc_state = Arc::new(ipc::IpcState::new(
        initial_status,
        ipc::TrayPolicy::default_permissive(),
        force_checkin_tx,
        stop_agent_tx,
    ));

    // Start IPC server for tray application
    let ipc_server_state = Arc::clone(&ipc_state);
    let ipc_handle = tokio::spawn(async move {
        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
            error!("IPC server error: {}", e);
        }
    });
    info!("IPC server started");

    // Start the WebSocket client with auto-reconnect
    let ws_state = Arc::clone(&state);
    let ws_handle = tokio::spawn(async move {
        loop {
            info!("Connecting to server...");
            match WebSocketClient::connect_and_run(Arc::clone(&ws_state)).await {
                Ok(_) => {
                    warn!("WebSocket connection closed normally, reconnecting...");
                }
                Err(e) => {
                    error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
                }
            }

            // Mark as disconnected
            *ws_state.connected.write().await = false;

            // Wait before reconnecting
            tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
        }
    });

    // Start metrics collection loop
    let metrics_state = Arc::clone(&state);
    let metrics_handle = tokio::spawn(async move {
        let interval = metrics_state.config.metrics.interval_seconds;
        let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval));

        loop {
            interval_timer.tick().await;

            // Collect metrics (they'll be sent via WebSocket if connected)
            let metrics = metrics_state.metrics_collector.collect().await;
            if *metrics_state.connected.read().await {
                info!(
                    "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
                    metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
                );
            }
        }
    });

    // Task to update IPC status periodically
    let ipc_update_state = Arc::clone(&state);
    let ipc_update_ipc = Arc::clone(&ipc_state);
    let ipc_update_handle = tokio::spawn(async move {
        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
        loop {
            interval.tick().await;

            let connected = *ipc_update_state.connected.read().await;
            let last_checkin = ipc_update_state.last_checkin.read().await.map(|dt| dt.to_rfc3339());

            let status = ipc::AgentStatus {
                connected,
                last_checkin,
                server_url: ipc_update_state.config.server.url.clone(),
                agent_version: env!("CARGO_PKG_VERSION").to_string(),
                device_id: device_id::get_device_id(),
                hostname: hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string()),
                error: if connected { None } else { Some("Disconnected".to_string()) },
            };

            ipc_update_ipc.update_status(status).await;
        }
    });

    // Wait for shutdown signal or stop request
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received shutdown signal");
        }
        _ = stop_agent_rx.recv() => {
            info!("Received stop request from tray");
        }
        _ = ws_handle => {
            error!("WebSocket task ended unexpectedly");
        }
        _ = metrics_handle => {
            error!("Metrics task ended unexpectedly");
        }
        _ = ipc_handle => {
            error!("IPC server ended unexpectedly");
        }
        _ = ipc_update_handle => {
            error!("IPC update task ended unexpectedly");
        }
    }

    info!("GuruRMM Agent shutting down");
    Ok(())
}

/// Install the agent as a system service
async fn install_service(
    server_url: Option<String>,
    api_key: Option<String>,
    skip_legacy_check: bool,
) -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::install(server_url, api_key, skip_legacy_check)
    }

    #[cfg(target_os = "linux")]
    {
        install_systemd_service(server_url, api_key, skip_legacy_check).await
    }

    #[cfg(target_os = "macos")]
    {
        let _ = (server_url, api_key, skip_legacy_check); // Suppress unused warnings
        info!("Installing GuruRMM Agent as launchd service...");
        todo!("macOS launchd service installation not yet implemented");
    }
}

/// Legacy service names to check for and clean up (Linux)
#[cfg(target_os = "linux")]
const LINUX_LEGACY_SERVICE_NAMES: &[&str] = &[
    "gururmm",           // Old name without -agent suffix
    "guru-rmm-agent",    // Alternative naming
    "GuruRMM-Agent",     // Case variant
];

/// Clean up legacy Linux service installations
#[cfg(target_os = "linux")]
fn cleanup_legacy_linux_services() -> Result<()> {
    use std::process::Command;

    info!("Checking for legacy service installations...");

    for legacy_name in LINUX_LEGACY_SERVICE_NAMES {
        // Check if service exists
        let status = Command::new("systemctl")
            .args(["status", legacy_name])
            .output();

        if let Ok(output) = status {
            if output.status.success() || String::from_utf8_lossy(&output.stderr).contains("Loaded:") {
                info!("Found legacy service '{}', removing...", legacy_name);

                // Stop the service
                let _ = Command::new("systemctl")
                    .args(["stop", legacy_name])
                    .status();

                // Disable the service
                let _ = Command::new("systemctl")
                    .args(["disable", legacy_name])
                    .status();

                // Remove unit file
                let unit_file = format!("/etc/systemd/system/{}.service", legacy_name);
                if std::path::Path::new(&unit_file).exists() {
                    info!("Removing legacy unit file: {}", unit_file);
                    let _ = std::fs::remove_file(&unit_file);
                }
            }
        }
    }

    // Check for legacy binaries in common locations
    let legacy_binary_locations = [
        "/usr/local/bin/gururmm",
        "/usr/bin/gururmm",
        "/opt/gururmm/gururmm",
        "/opt/gururmm/agent",
    ];

    for legacy_path in legacy_binary_locations {
        if std::path::Path::new(legacy_path).exists() {
            info!("Found legacy binary at '{}', removing...", legacy_path);
            let _ = std::fs::remove_file(legacy_path);
        }
    }

    // Reload systemd to pick up removed unit files
    let _ = Command::new("systemctl")
        .args(["daemon-reload"])
        .status();

    Ok(())
}

/// Install as a systemd service (Linux)
#[cfg(target_os = "linux")]
async fn install_systemd_service(
    server_url: Option<String>,
    api_key: Option<String>,
    skip_legacy_check: bool,
) -> Result<()> {
    use std::process::Command;

    const SERVICE_NAME: &str = "gururmm-agent";
    const INSTALL_DIR: &str = "/usr/local/bin";
    const CONFIG_DIR: &str = "/etc/gururmm";
    const SYSTEMD_DIR: &str = "/etc/systemd/system";

    info!("Installing GuruRMM Agent as systemd service...");

    // Check if running as root
    if !nix::unistd::geteuid().is_root() {
        anyhow::bail!("Installation requires root privileges. Please run with sudo.");
    }

    // Clean up legacy installations unless skipped
    if !skip_legacy_check {
        if let Err(e) = cleanup_legacy_linux_services() {
            warn!("Legacy cleanup warning: {}", e);
        }
    }

    // Get the current executable path
    let current_exe = std::env::current_exe()
        .context("Failed to get current executable path")?;

    let binary_dest = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
    let config_dest = format!("{}/agent.toml", CONFIG_DIR);
    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);

    // Create config directory
    info!("Creating config directory: {}", CONFIG_DIR);
    std::fs::create_dir_all(CONFIG_DIR)
        .context("Failed to create config directory")?;

    // Copy binary
    info!("Copying binary to: {}", binary_dest);
    std::fs::copy(&current_exe, &binary_dest)
        .context("Failed to copy binary")?;

    // Make binary executable
    Command::new("chmod")
        .args(["+x", &binary_dest])
        .status()
        .context("Failed to set binary permissions")?;

    // Handle configuration
    let config_needs_manual_edit;
    if !std::path::Path::new(&config_dest).exists() {
        info!("Creating config: {}", config_dest);

        // Start with sample config
        let mut config = crate::config::AgentConfig::sample();

        // Apply provided values
        if let Some(url) = &server_url {
            config.server.url = url.clone();
        }
        if let Some(key) = &api_key {
            config.server.api_key = key.clone();
        }

        let toml_str = toml::to_string_pretty(&config)?;
        std::fs::write(&config_dest, toml_str)
            .context("Failed to write config file")?;

        // Set restrictive permissions on config (contains API key)
        Command::new("chmod")
            .args(["600", &config_dest])
            .status()
            .context("Failed to set config permissions")?;

        config_needs_manual_edit = server_url.is_none() || api_key.is_none();
    } else {
        info!("Config already exists: {}", config_dest);
        config_needs_manual_edit = false;

        // If server_url or api_key provided, update existing config
        if server_url.is_some() || api_key.is_some() {
            info!("Updating existing configuration...");
            let config_content = std::fs::read_to_string(&config_dest)?;
            let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
                .context("Failed to parse existing config")?;

            if let Some(url) = &server_url {
                config.server.url = url.clone();
            }
            if let Some(key) = &api_key {
                config.server.api_key = key.clone();
            }

            let toml_str = toml::to_string_pretty(&config)?;
            std::fs::write(&config_dest, toml_str)
                .context("Failed to update config file")?;
        }
    }

    // Create systemd unit file
    let unit_content = format!(r#"[Unit]
Description=GuruRMM Agent - Remote Monitoring and Management
Documentation=https://github.com/azcomputerguru/gururmm
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart={binary} --config {config} run
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier={service}

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
ReadWritePaths=/var/log

[Install]
WantedBy=multi-user.target
"#,
        binary = binary_dest,
        config = config_dest,
        service = SERVICE_NAME
    );

    info!("Creating systemd unit file: {}", unit_file);
    std::fs::write(&unit_file, unit_content)
        .context("Failed to write systemd unit file")?;

    // Reload systemd daemon
    info!("Reloading systemd daemon...");
    let status = Command::new("systemctl")
        .args(["daemon-reload"])
        .status()
        .context("Failed to reload systemd")?;

    if !status.success() {
        anyhow::bail!("systemctl daemon-reload failed");
    }

    // Enable the service
    info!("Enabling service...");
    let status = Command::new("systemctl")
        .args(["enable", SERVICE_NAME])
        .status()
        .context("Failed to enable service")?;

    if !status.success() {
        anyhow::bail!("systemctl enable failed");
    }

    println!("\n✓ GuruRMM Agent installed successfully!");
    println!("\nInstalled files:");
    println!("  Binary: {}", binary_dest);
    println!("  Config: {}", config_dest);
    println!("  Service: {}", unit_file);

    if config_needs_manual_edit {
        println!("\n⚠  IMPORTANT: Edit {} with your server URL and API key!", config_dest);
        println!("\nNext steps:");
        println!("  1. Edit {} with your server URL and API key", config_dest);
        println!("  2. Start the service: sudo systemctl start {}", SERVICE_NAME);
    } else {
        println!("\nStarting service...");
        let status = Command::new("systemctl")
            .args(["start", SERVICE_NAME])
            .status();

        if status.is_ok() && status.unwrap().success() {
            println!("✓ Service started successfully!");
        } else {
            println!("⚠  Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
        }
    }

    println!("\nUseful commands:");
    println!("  Status: sudo systemctl status {}", SERVICE_NAME);
    println!("  Logs:   sudo journalctl -u {} -f", SERVICE_NAME);
    println!("  Stop:   sudo systemctl stop {}", SERVICE_NAME);
    println!("  Start:  sudo systemctl start {}", SERVICE_NAME);

    Ok(())
}

/// Uninstall the system service
async fn uninstall_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::uninstall()
    }

    #[cfg(target_os = "linux")]
    {
        uninstall_systemd_service().await
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service uninstallation not yet implemented");
    }
}

/// Uninstall systemd service (Linux)
#[cfg(target_os = "linux")]
async fn uninstall_systemd_service() -> Result<()> {
    use std::process::Command;

    const SERVICE_NAME: &str = "gururmm-agent";
    const INSTALL_DIR: &str = "/usr/local/bin";
    const CONFIG_DIR: &str = "/etc/gururmm";
    const SYSTEMD_DIR: &str = "/etc/systemd/system";

    info!("Uninstalling GuruRMM Agent...");

    if !nix::unistd::geteuid().is_root() {
        anyhow::bail!("Uninstallation requires root privileges. Please run with sudo.");
    }

    let binary_path = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);

    // Stop the service if running
    info!("Stopping service...");
    let _ = Command::new("systemctl")
        .args(["stop", SERVICE_NAME])
        .status();

    // Disable the service
    info!("Disabling service...");
    let _ = Command::new("systemctl")
        .args(["disable", SERVICE_NAME])
        .status();

    // Remove unit file
    if std::path::Path::new(&unit_file).exists() {
        info!("Removing unit file: {}", unit_file);
        std::fs::remove_file(&unit_file)?;
    }

    // Remove binary
    if std::path::Path::new(&binary_path).exists() {
        info!("Removing binary: {}", binary_path);
        std::fs::remove_file(&binary_path)?;
    }

    // Reload systemd
    let _ = Command::new("systemctl")
        .args(["daemon-reload"])
        .status();

    println!("\n✓ GuruRMM Agent uninstalled successfully!");
    println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
    println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);

    Ok(())
}

/// Start the installed service
async fn start_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::start()
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::Command;

        info!("Starting GuruRMM Agent service...");

        let status = Command::new("systemctl")
            .args(["start", "gururmm-agent"])
            .status()
            .context("Failed to start service")?;

        if status.success() {
            println!("** Service started successfully");
            println!("Check status: sudo systemctl status gururmm-agent");
        } else {
            anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service start not yet implemented");
    }
}

/// Stop the installed service
async fn stop_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::stop()
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::Command;

        info!("Stopping GuruRMM Agent service...");

        let status = Command::new("systemctl")
            .args(["stop", "gururmm-agent"])
            .status()
            .context("Failed to stop service")?;

        if status.success() {
            println!("** Service stopped successfully");
        } else {
            anyhow::bail!("Failed to stop service");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service stop not yet implemented");
    }
}

/// Show agent status
async fn show_status(config_path: PathBuf) -> Result<()> {
    // On Windows, show service status
    #[cfg(windows)]
    {
        service::windows::status()?;
        println!();
    }

    // Try to load config for additional info
    match AgentConfig::load(&config_path) {
        Ok(config) => {
            println!("Configuration");
            println!("=============");
            println!("Config file: {:?}", config_path);
            println!("Server URL: {}", config.server.url);
            println!("Metrics interval: {} seconds", config.metrics.interval_seconds);
            println!("Watchdog enabled: {}", config.watchdog.enabled);

            // Collect current metrics
            let collector = MetricsCollector::new();
            let metrics = collector.collect().await;

            println!("\nCurrent System Metrics:");
            println!("  CPU Usage: {:.1}%", metrics.cpu_percent);
            println!("  Memory Usage: {:.1}%", metrics.memory_percent);
            println!(
                "  Memory Used: {:.2} GB",
                metrics.memory_used_bytes as f64 / 1_073_741_824.0
            );
            println!("  Disk Usage: {:.1}%", metrics.disk_percent);
            println!(
                "  Disk Used: {:.2} GB",
                metrics.disk_used_bytes as f64 / 1_073_741_824.0
            );
        }
        Err(_) => {
            println!("\nConfig file {:?} not found or invalid.", config_path);
            #[cfg(windows)]
            println!("Service config location: {}\\agent.toml", service::windows::CONFIG_DIR);
        }
    }

    Ok(())
}

/// Generate a sample configuration file
async fn generate_config(output: PathBuf) -> Result<()> {
    let sample_config = AgentConfig::sample();
    let toml_str = toml::to_string_pretty(&sample_config)?;

    std::fs::write(&output, toml_str)?;
    println!("Sample configuration written to {:?}", output);
    println!("\nEdit this file with your server URL and API key, then run:");
    println!("  gururmm-agent --config {:?} run", output);

    Ok(())
}

+    // Start tray launcher (Windows only for now)

+        let tray_exe_path = std::path::PathBuf::from(

+            format!("{}\\gururmm-tray.exe", service::windows::INSTALL_DIR)

+        let launcher = Arc::new(tray_launcher::TrayLauncher::new(tray_exe_path));

+        // Launch tray in existing sessions

+        let launcher_init = Arc::clone(&launcher);

+            // Small delay to let IPC server fully start

+            tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

+            launcher_init.ensure_running().await;

+        // Periodic check for new sessions (every 30 seconds)

+        let launcher_periodic = Arc::clone(&launcher);

+            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));

+                launcher_periodic.ensure_running().await;

+        info!("Tray launcher initialized");

+    let tray_launcher: Option<Arc<tray_launcher::TrayLauncher>> = None;


Now I need to also suppress the unused warning for `tray_launcher` when it's not used (on Windows it may not be used in select! block). Let me also add termination of tray processes on shutdown. Let me check the shutdown handling code and update it.


Now let me update the shutdown handling to terminate tray processes:


    // Wait for shutdown signal or stop request
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received shutdown signal");
        }
        _ = stop_agent_rx.recv() => {
            info!("Received stop request from tray");
        }
        _ = ws_handle => {
            error!("WebSocket task ended unexpectedly");
        }
        _ = metrics_handle => {
            error!("Metrics task ended unexpectedly");
        }
        _ = ipc_handle => {
            error!("IPC server ended unexpectedly");
        }
        _ = ipc_update_handle => {
            error!("IPC update task ended unexpectedly");
        }
    }

    info!("GuruRMM Agent shutting down");
    Ok(())

    // Wait for shutdown signal or stop request
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received shutdown signal");
        }
        _ = stop_agent_rx.recv() => {
            info!("Received stop request from tray");
        }
        _ = ws_handle => {
            error!("WebSocket task ended unexpectedly");
        }
        _ = metrics_handle => {
            error!("Metrics task ended unexpectedly");
        }
        _ = ipc_handle => {
            error!("IPC server ended unexpectedly");
        }
        _ = ipc_update_handle => {
            error!("IPC update task ended unexpectedly");
        }
    }

    // Terminate tray processes on shutdown
    #[cfg(windows)]
    if let Some(launcher) = tray_launcher {
        info!("Terminating tray processes...");
        launcher.terminate_all().await;
    }

    #[cfg(not(windows))]
    let _ = tray_launcher; // Suppress unused warning

    info!("GuruRMM Agent shutting down");
    Ok(())


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\main.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   289→            ipc_update_ipc.update_status(status).await;
   290→        }
   291→    });
   292→
   293→    // Wait for shutdown signal or stop request
   294→    tokio::select! {
   295→        _ = tokio::signal::ctrl_c() => {
   296→            info!("Received shutdown signal");
   297→        }
   298→        _ = stop_agent_rx.recv() => {
   299→            info!("Received stop request from tray");
   300→        }
   301→        _ = ws_handle => {
   302→            error!("WebSocket task ended unexpectedly");
   303→        }
   304→        _ = metrics_handle => {
   305→            error!("Metrics task ended unexpectedly");
   306→        }
   307→        _ = ipc_handle => {
   308→            error!("IPC server ended unexpectedly");
   309→        }
   310→        _ = ipc_update_handle => {
   311→            error!("IPC update task ended unexpectedly");
   312→        }
   313→    }
   314→
   315→    // Terminate tray processes on shutdown
   316→    #[cfg(windows)]
   317→    if let Some(launcher) = tray_launcher {
   318→        info!("Terminating tray processes...");
   319→        launcher.terminate_all().await;
   320→    }
   321→
   322→    #[cfg(not(windows))]
   323→    let _ = tray_launcher; // Suppress unused warning
   324→
   325→    info!("GuruRMM Agent shutting down");
   326→    Ok(())
   327→}
   328→
   329→/// Install the agent as a system service
   330→async fn install_service(

    // Wait for shutdown signal or stop request
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received shutdown signal");
        }
        _ = stop_agent_rx.recv() => {
            info!("Received stop request from tray");
        }
        _ = ws_handle => {
            error!("WebSocket task ended unexpectedly");
        }
        _ = metrics_handle => {
            error!("Metrics task ended unexpectedly");
        }
        _ = ipc_handle => {
            error!("IPC server ended unexpectedly");
        }
        _ = ipc_update_handle => {
            error!("IPC update task ended unexpectedly");
        }
    }

    // Terminate tray processes on shutdown
    #[cfg(windows)]
    if let Some(launcher) = tray_launcher {
        info!("Terminating tray processes...");
        launcher.terminate_all().await;
    }

    #[cfg(not(windows))]
    let _ = tray_launcher; // Suppress unused warning

    info!("GuruRMM Agent shutting down");
    Ok(())

//! GuruRMM Agent - Cross-platform Remote Monitoring and Management Agent
//!
//! This agent connects to the GuruRMM server, reports system metrics,
//! monitors services (watchdog), and executes remote commands.

mod config;
mod device_id;
mod ipc;
mod metrics;
mod service;
mod transport;
mod tray_launcher;
mod updater;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};

use crate::config::AgentConfig;
use crate::metrics::MetricsCollector;
use crate::transport::WebSocketClient;

/// GuruRMM Agent - Remote Monitoring and Management
#[derive(Parser)]
#[command(name = "gururmm-agent")]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Path to configuration file
    #[arg(short, long, default_value = "agent.toml")]
    config: PathBuf,

    /// Subcommand to run
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Run the agent (default)
    Run,

    /// Install as a system service
    Install {
        /// Server WebSocket URL (e.g., wss://rmm-api.example.com/ws)
        #[arg(long)]
        server_url: Option<String>,

        /// API key for authentication
        #[arg(long)]
        api_key: Option<String>,

        /// Skip legacy service detection and cleanup
        #[arg(long, default_value = "false")]
        skip_legacy_check: bool,
    },

    /// Uninstall the system service
    Uninstall,

    /// Start the installed service
    Start,

    /// Stop the installed service
    Stop,

    /// Show agent status
    Status,

    /// Generate a sample configuration file
    GenerateConfig {
        /// Output path for config file
        #[arg(short, long, default_value = "agent.toml")]
        output: PathBuf,
    },

    /// Run as Windows service (called by SCM, not for manual use)
    #[command(hide = true)]
    Service,
}

/// Shared application state
pub struct AppState {
    pub config: AgentConfig,
    pub metrics_collector: MetricsCollector,
    pub connected: RwLock<bool>,
    pub last_checkin: RwLock<Option<chrono::DateTime<chrono::Utc>>>,
    pub tray_policy: RwLock<ipc::TrayPolicy>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("gururmm_agent=info".parse()?)
                .add_directive("info".parse()?),
        )
        .init();

    let cli = Cli::parse();

    match cli.command.unwrap_or(Commands::Run) {
        Commands::Run => run_agent(cli.config).await,
        Commands::Install { server_url, api_key, skip_legacy_check } => {
            install_service(server_url, api_key, skip_legacy_check).await
        }
        Commands::Uninstall => uninstall_service().await,
        Commands::Start => start_service().await,
        Commands::Stop => stop_service().await,
        Commands::Status => show_status(cli.config).await,
        Commands::GenerateConfig { output } => generate_config(output).await,
        Commands::Service => run_as_windows_service(),
    }
}

/// Run as a Windows service (called by SCM)
fn run_as_windows_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::run_as_service()
    }

    #[cfg(not(windows))]
    {
        anyhow::bail!("Windows service mode is only available on Windows");
    }
}

/// Main agent runtime loop
async fn run_agent(config_path: PathBuf) -> Result<()> {
    info!("GuruRMM Agent starting...");

    // Load configuration
    let config = AgentConfig::load(&config_path)?;
    info!("Loaded configuration from {:?}", config_path);
    info!("Server URL: {}", config.server.url);

    // Initialize metrics collector
    let metrics_collector = MetricsCollector::new();
    info!("Metrics collector initialized");

    // Create channels for IPC commands
    let (force_checkin_tx, mut force_checkin_rx) = tokio::sync::mpsc::channel::<()>(8);
    let (stop_agent_tx, mut stop_agent_rx) = tokio::sync::mpsc::channel::<()>(1);

    // Create shared state
    let state = Arc::new(AppState {
        config: config.clone(),
        metrics_collector,
        connected: RwLock::new(false),
        last_checkin: RwLock::new(None),
        tray_policy: RwLock::new(ipc::TrayPolicy::default_permissive()),
    });

    // Create IPC state for tray communication
    let device_id = device_id::get_device_id();
    let hostname = hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    let initial_status = ipc::AgentStatus {
        connected: false,
        last_checkin: None,
        server_url: config.server.url.clone(),
        agent_version: env!("CARGO_PKG_VERSION").to_string(),
        device_id,
        hostname,
        error: None,
    };

    let ipc_state = Arc::new(ipc::IpcState::new(
        initial_status,
        ipc::TrayPolicy::default_permissive(),
        force_checkin_tx,
        stop_agent_tx,
    ));

    // Start IPC server for tray application
    let ipc_server_state = Arc::clone(&ipc_state);
    let ipc_handle = tokio::spawn(async move {
        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
            error!("IPC server error: {}", e);
        }
    });
    info!("IPC server started");

    // Start tray launcher (Windows only for now)
    #[cfg(windows)]
    let tray_launcher = {
        let tray_exe_path = std::path::PathBuf::from(
            format!("{}\\gururmm-tray.exe", service::windows::INSTALL_DIR)
        );
        let launcher = Arc::new(tray_launcher::TrayLauncher::new(tray_exe_path));

        // Launch tray in existing sessions
        let launcher_init = Arc::clone(&launcher);
        tokio::spawn(async move {
            // Small delay to let IPC server fully start
            tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
            launcher_init.ensure_running().await;
        });

        // Periodic check for new sessions (every 30 seconds)
        let launcher_periodic = Arc::clone(&launcher);
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
            loop {
                interval.tick().await;
                launcher_periodic.ensure_running().await;
            }
        });

        info!("Tray launcher initialized");
        Some(launcher)
    };

    #[cfg(not(windows))]
    let tray_launcher: Option<Arc<tray_launcher::TrayLauncher>> = None;

    // Start the WebSocket client with auto-reconnect
    let ws_state = Arc::clone(&state);
    let ws_handle = tokio::spawn(async move {
        loop {
            info!("Connecting to server...");
            match WebSocketClient::connect_and_run(Arc::clone(&ws_state)).await {
                Ok(_) => {
                    warn!("WebSocket connection closed normally, reconnecting...");
                }
                Err(e) => {
                    error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
                }
            }

            // Mark as disconnected
            *ws_state.connected.write().await = false;

            // Wait before reconnecting
            tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
        }
    });

    // Start metrics collection loop
    let metrics_state = Arc::clone(&state);
    let metrics_handle = tokio::spawn(async move {
        let interval = metrics_state.config.metrics.interval_seconds;
        let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval));

        loop {
            interval_timer.tick().await;

            // Collect metrics (they'll be sent via WebSocket if connected)
            let metrics = metrics_state.metrics_collector.collect().await;
            if *metrics_state.connected.read().await {
                info!(
                    "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
                    metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
                );
            }
        }
    });

    // Task to update IPC status periodically
    let ipc_update_state = Arc::clone(&state);
    let ipc_update_ipc = Arc::clone(&ipc_state);
    let ipc_update_handle = tokio::spawn(async move {
        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
        loop {
            interval.tick().await;

            let connected = *ipc_update_state.connected.read().await;
            let last_checkin = ipc_update_state.last_checkin.read().await.map(|dt| dt.to_rfc3339());

            let status = ipc::AgentStatus {
                connected,
                last_checkin,
                server_url: ipc_update_state.config.server.url.clone(),
                agent_version: env!("CARGO_PKG_VERSION").to_string(),
                device_id: device_id::get_device_id(),
                hostname: hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string()),
                error: if connected { None } else { Some("Disconnected".to_string()) },
            };

            ipc_update_ipc.update_status(status).await;
        }
    });

    // Wait for shutdown signal or stop request
    tokio::select! {
        _ = tokio::signal::ctrl_c() => {
            info!("Received shutdown signal");
        }
        _ = stop_agent_rx.recv() => {
            info!("Received stop request from tray");
        }
        _ = ws_handle => {
            error!("WebSocket task ended unexpectedly");
        }
        _ = metrics_handle => {
            error!("Metrics task ended unexpectedly");
        }
        _ = ipc_handle => {
            error!("IPC server ended unexpectedly");
        }
        _ = ipc_update_handle => {
            error!("IPC update task ended unexpectedly");
        }
    }

    info!("GuruRMM Agent shutting down");
    Ok(())
}

/// Install the agent as a system service
async fn install_service(
    server_url: Option<String>,
    api_key: Option<String>,
    skip_legacy_check: bool,
) -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::install(server_url, api_key, skip_legacy_check)
    }

    #[cfg(target_os = "linux")]
    {
        install_systemd_service(server_url, api_key, skip_legacy_check).await
    }

    #[cfg(target_os = "macos")]
    {
        let _ = (server_url, api_key, skip_legacy_check); // Suppress unused warnings
        info!("Installing GuruRMM Agent as launchd service...");
        todo!("macOS launchd service installation not yet implemented");
    }
}

/// Legacy service names to check for and clean up (Linux)
#[cfg(target_os = "linux")]
const LINUX_LEGACY_SERVICE_NAMES: &[&str] = &[
    "gururmm",           // Old name without -agent suffix
    "guru-rmm-agent",    // Alternative naming
    "GuruRMM-Agent",     // Case variant
];

/// Clean up legacy Linux service installations
#[cfg(target_os = "linux")]
fn cleanup_legacy_linux_services() -> Result<()> {
    use std::process::Command;

    info!("Checking for legacy service installations...");

    for legacy_name in LINUX_LEGACY_SERVICE_NAMES {
        // Check if service exists
        let status = Command::new("systemctl")
            .args(["status", legacy_name])
            .output();

        if let Ok(output) = status {
            if output.status.success() || String::from_utf8_lossy(&output.stderr).contains("Loaded:") {
                info!("Found legacy service '{}', removing...", legacy_name);

                // Stop the service
                let _ = Command::new("systemctl")
                    .args(["stop", legacy_name])
                    .status();

                // Disable the service
                let _ = Command::new("systemctl")
                    .args(["disable", legacy_name])
                    .status();

                // Remove unit file
                let unit_file = format!("/etc/systemd/system/{}.service", legacy_name);
                if std::path::Path::new(&unit_file).exists() {
                    info!("Removing legacy unit file: {}", unit_file);
                    let _ = std::fs::remove_file(&unit_file);
                }
            }
        }
    }

    // Check for legacy binaries in common locations
    let legacy_binary_locations = [
        "/usr/local/bin/gururmm",
        "/usr/bin/gururmm",
        "/opt/gururmm/gururmm",
        "/opt/gururmm/agent",
    ];

    for legacy_path in legacy_binary_locations {
        if std::path::Path::new(legacy_path).exists() {
            info!("Found legacy binary at '{}', removing...", legacy_path);
            let _ = std::fs::remove_file(legacy_path);
        }
    }

    // Reload systemd to pick up removed unit files
    let _ = Command::new("systemctl")
        .args(["daemon-reload"])
        .status();

    Ok(())
}

/// Install as a systemd service (Linux)
#[cfg(target_os = "linux")]
async fn install_systemd_service(
    server_url: Option<String>,
    api_key: Option<String>,
    skip_legacy_check: bool,
) -> Result<()> {
    use std::process::Command;

    const SERVICE_NAME: &str = "gururmm-agent";
    const INSTALL_DIR: &str = "/usr/local/bin";
    const CONFIG_DIR: &str = "/etc/gururmm";
    const SYSTEMD_DIR: &str = "/etc/systemd/system";

    info!("Installing GuruRMM Agent as systemd service...");

    // Check if running as root
    if !nix::unistd::geteuid().is_root() {
        anyhow::bail!("Installation requires root privileges. Please run with sudo.");
    }

    // Clean up legacy installations unless skipped
    if !skip_legacy_check {
        if let Err(e) = cleanup_legacy_linux_services() {
            warn!("Legacy cleanup warning: {}", e);
        }
    }

    // Get the current executable path
    let current_exe = std::env::current_exe()
        .context("Failed to get current executable path")?;

    let binary_dest = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
    let config_dest = format!("{}/agent.toml", CONFIG_DIR);
    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);

    // Create config directory
    info!("Creating config directory: {}", CONFIG_DIR);
    std::fs::create_dir_all(CONFIG_DIR)
        .context("Failed to create config directory")?;

    // Copy binary
    info!("Copying binary to: {}", binary_dest);
    std::fs::copy(&current_exe, &binary_dest)
        .context("Failed to copy binary")?;

    // Make binary executable
    Command::new("chmod")
        .args(["+x", &binary_dest])
        .status()
        .context("Failed to set binary permissions")?;

    // Handle configuration
    let config_needs_manual_edit;
    if !std::path::Path::new(&config_dest).exists() {
        info!("Creating config: {}", config_dest);

        // Start with sample config
        let mut config = crate::config::AgentConfig::sample();

        // Apply provided values
        if let Some(url) = &server_url {
            config.server.url = url.clone();
        }
        if let Some(key) = &api_key {
            config.server.api_key = key.clone();
        }

        let toml_str = toml::to_string_pretty(&config)?;
        std::fs::write(&config_dest, toml_str)
            .context("Failed to write config file")?;

        // Set restrictive permissions on config (contains API key)
        Command::new("chmod")
            .args(["600", &config_dest])
            .status()
            .context("Failed to set config permissions")?;

        config_needs_manual_edit = server_url.is_none() || api_key.is_none();
    } else {
        info!("Config already exists: {}", config_dest);
        config_needs_manual_edit = false;

        // If server_url or api_key provided, update existing config
        if server_url.is_some() || api_key.is_some() {
            info!("Updating existing configuration...");
            let config_content = std::fs::read_to_string(&config_dest)?;
            let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
                .context("Failed to parse existing config")?;

            if let Some(url) = &server_url {
                config.server.url = url.clone();
            }
            if let Some(key) = &api_key {
                config.server.api_key = key.clone();
            }

            let toml_str = toml::to_string_pretty(&config)?;
            std::fs::write(&config_dest, toml_str)
                .context("Failed to update config file")?;
        }
    }

    // Create systemd unit file
    let unit_content = format!(r#"[Unit]
Description=GuruRMM Agent - Remote Monitoring and Management
Documentation=https://github.com/azcomputerguru/gururmm
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart={binary} --config {config} run
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier={service}

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
ReadWritePaths=/var/log

[Install]
WantedBy=multi-user.target
"#,
        binary = binary_dest,
        config = config_dest,
        service = SERVICE_NAME
    );

    info!("Creating systemd unit file: {}", unit_file);
    std::fs::write(&unit_file, unit_content)
        .context("Failed to write systemd unit file")?;

    // Reload systemd daemon
    info!("Reloading systemd daemon...");
    let status = Command::new("systemctl")
        .args(["daemon-reload"])
        .status()
        .context("Failed to reload systemd")?;

    if !status.success() {
        anyhow::bail!("systemctl daemon-reload failed");
    }

    // Enable the service
    info!("Enabling service...");
    let status = Command::new("systemctl")
        .args(["enable", SERVICE_NAME])
        .status()
        .context("Failed to enable service")?;

    if !status.success() {
        anyhow::bail!("systemctl enable failed");
    }

    println!("\n✓ GuruRMM Agent installed successfully!");
    println!("\nInstalled files:");
    println!("  Binary: {}", binary_dest);
    println!("  Config: {}", config_dest);
    println!("  Service: {}", unit_file);

    if config_needs_manual_edit {
        println!("\n⚠  IMPORTANT: Edit {} with your server URL and API key!", config_dest);
        println!("\nNext steps:");
        println!("  1. Edit {} with your server URL and API key", config_dest);
        println!("  2. Start the service: sudo systemctl start {}", SERVICE_NAME);
    } else {
        println!("\nStarting service...");
        let status = Command::new("systemctl")
            .args(["start", SERVICE_NAME])
            .status();

        if status.is_ok() && status.unwrap().success() {
            println!("✓ Service started successfully!");
        } else {
            println!("⚠  Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
        }
    }

    println!("\nUseful commands:");
    println!("  Status: sudo systemctl status {}", SERVICE_NAME);
    println!("  Logs:   sudo journalctl -u {} -f", SERVICE_NAME);
    println!("  Stop:   sudo systemctl stop {}", SERVICE_NAME);
    println!("  Start:  sudo systemctl start {}", SERVICE_NAME);

    Ok(())
}

/// Uninstall the system service
async fn uninstall_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::uninstall()
    }

    #[cfg(target_os = "linux")]
    {
        uninstall_systemd_service().await
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service uninstallation not yet implemented");
    }
}

/// Uninstall systemd service (Linux)
#[cfg(target_os = "linux")]
async fn uninstall_systemd_service() -> Result<()> {
    use std::process::Command;

    const SERVICE_NAME: &str = "gururmm-agent";
    const INSTALL_DIR: &str = "/usr/local/bin";
    const CONFIG_DIR: &str = "/etc/gururmm";
    const SYSTEMD_DIR: &str = "/etc/systemd/system";

    info!("Uninstalling GuruRMM Agent...");

    if !nix::unistd::geteuid().is_root() {
        anyhow::bail!("Uninstallation requires root privileges. Please run with sudo.");
    }

    let binary_path = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);

    // Stop the service if running
    info!("Stopping service...");
    let _ = Command::new("systemctl")
        .args(["stop", SERVICE_NAME])
        .status();

    // Disable the service
    info!("Disabling service...");
    let _ = Command::new("systemctl")
        .args(["disable", SERVICE_NAME])
        .status();

    // Remove unit file
    if std::path::Path::new(&unit_file).exists() {
        info!("Removing unit file: {}", unit_file);
        std::fs::remove_file(&unit_file)?;
    }

    // Remove binary
    if std::path::Path::new(&binary_path).exists() {
        info!("Removing binary: {}", binary_path);
        std::fs::remove_file(&binary_path)?;
    }

    // Reload systemd
    let _ = Command::new("systemctl")
        .args(["daemon-reload"])
        .status();

    println!("\n✓ GuruRMM Agent uninstalled successfully!");
    println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
    println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);

    Ok(())
}

/// Start the installed service
async fn start_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::start()
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::Command;

        info!("Starting GuruRMM Agent service...");

        let status = Command::new("systemctl")
            .args(["start", "gururmm-agent"])
            .status()
            .context("Failed to start service")?;

        if status.success() {
            println!("** Service started successfully");
            println!("Check status: sudo systemctl status gururmm-agent");
        } else {
            anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service start not yet implemented");
    }
}

/// Stop the installed service
async fn stop_service() -> Result<()> {
    #[cfg(windows)]
    {
        service::windows::stop()
    }

    #[cfg(target_os = "linux")]
    {
        use std::process::Command;

        info!("Stopping GuruRMM Agent service...");

        let status = Command::new("systemctl")
            .args(["stop", "gururmm-agent"])
            .status()
            .context("Failed to stop service")?;

        if status.success() {
            println!("** Service stopped successfully");
        } else {
            anyhow::bail!("Failed to stop service");
        }

        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        todo!("macOS service stop not yet implemented");
    }
}

/// Show agent status
async fn show_status(config_path: PathBuf) -> Result<()> {
    // On Windows, show service status
    #[cfg(windows)]
    {
        service::windows::status()?;
        println!();
    }

    // Try to load config for additional info
    match AgentConfig::load(&config_path) {
        Ok(config) => {
            println!("Configuration");
            println!("=============");
            println!("Config file: {:?}", config_path);
            println!("Server URL: {}", config.server.url);
            println!("Metrics interval: {} seconds", config.metrics.interval_seconds);
            println!("Watchdog enabled: {}", config.watchdog.enabled);

            // Collect current metrics
            let collector = MetricsCollector::new();
            let metrics = collector.collect().await;

            println!("\nCurrent System Metrics:");
            println!("  CPU Usage: {:.1}%", metrics.cpu_percent);
            println!("  Memory Usage: {:.1}%", metrics.memory_percent);
            println!(
                "  Memory Used: {:.2} GB",
                metrics.memory_used_bytes as f64 / 1_073_741_824.0
            );
            println!("  Disk Usage: {:.1}%", metrics.disk_percent);
            println!(
                "  Disk Used: {:.2} GB",
                metrics.disk_used_bytes as f64 / 1_073_741_824.0
            );
        }
        Err(_) => {
            println!("\nConfig file {:?} not found or invalid.", config_path);
            #[cfg(windows)]
            println!("Service config location: {}\\agent.toml", service::windows::CONFIG_DIR);
        }
    }

    Ok(())
}

/// Generate a sample configuration file
async fn generate_config(output: PathBuf) -> Result<()> {
    let sample_config = AgentConfig::sample();
    let toml_str = toml::to_string_pretty(&sample_config)?;

    std::fs::write(&output, toml_str)?;
    println!("Sample configuration written to {:?}", output);
    println!("\nEdit this file with your server URL and API key, then run:");
    println!("  gururmm-agent --config {:?} run", output);

    Ok(())
}

+    // Terminate tray processes on shutdown

+    if let Some(launcher) = tray_launcher {

+        info!("Terminating tray processes...");

+        launcher.terminate_all().await;

+    let _ = tray_launcher; // Suppress unused warning


Good progress. Now I need to:
1. Update the Windows service code (service.rs) to also use the tray launcher since that's the main way the agent runs in production
2. Update the install command to copy the tray binary

Let me update service.rs to integrate the tray launcher there too, since that's where the Windows service runs.


TodoWrite

Create tray_launcher.rs module with Windows session enumeration

Implement CreateProcessAsUser to launch tray in user sessions

Add session change monitoring (logon/logoff events)

Integrate tray launcher into agent main loop

Integrate tray launcher into Windows service mode

Update install command to bundle/copy tray binary

Build and test tray launcher functionality


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create tray_launcher.rs module with Windows session enumeration

Implement CreateProcessAsUser to launch tray in user sessions

Add session change monitoring (logon/logoff events)

Integrate tray launcher into agent main loop

Update install command to bundle/copy tray binary

Test tray launcher functionality

Integrate tray launcher into Windows service mode

Build and test tray launcher functionality


Now I need to integrate the tray launcher into the Windows service code. Let me update service.rs.


Now let me integrate the tray launcher into the Windows service mode:


    90→            .context("Failed to set StartPending status")?;
    91→
    92→        // Determine config path
    93→        let config_path = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR));
    94→
    95→        // Create the tokio runtime for the agent
    96→        let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
    97→
    98→        // Start the agent in the runtime
    99→        let agent_result = runtime.block_on(async {
   100→            // Load configuration
   101→            let config = match crate::config::AgentConfig::load(&config_path) {
   102→                Ok(c) => c,
   103→                Err(e) => {
   104→                    error!("Failed to load config from {:?}: {}", config_path, e);
   105→                    return Err(anyhow::anyhow!("Config load failed: {}", e));
   106→                }
   107→            };
   108→
   109→            info!("GuruRMM Agent service starting...");
   110→            info!("Config loaded from {:?}", config_path);
   111→            info!("Server URL: {}", config.server.url);
   112→
   113→            // Initialize metrics collector
   114→            let metrics_collector = crate::metrics::MetricsCollector::new();
   115→            info!("Metrics collector initialized");
   116→
   117→            // Create shared state
   118→            let state = std::sync::Arc::new(crate::AppState {
   119→                config: config.clone(),
   120→                metrics_collector,
   121→                connected: tokio::sync::RwLock::new(false),
   122→                last_checkin: tokio::sync::RwLock::new(None),
   123→                tray_policy: tokio::sync::RwLock::new(crate::ipc::TrayPolicy::default_permissive()),
   124→            });
   125→
   126→            // Report that we're running
   127→            status_handle
   128→                .set_service_status(ServiceStatus {
   129→                    service_type: ServiceType::OWN_PROCESS,
   130→                    current_state: ServiceState::Running,
   131→                    controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
   132→                    exit_code: ServiceExitCode::Win32(0),
   133→                    checkpoint: 0,
   134→                    wait_hint: Duration::default(),
   135→                    process_id: None,
   136→                })
   137→                .context("Failed to set Running status")?;
   138→
   139→            // Start WebSocket client task
   140→            let ws_state = std::sync::Arc::clone(&state);
   141→            let ws_handle = tokio::spawn(async move {
   142→                loop {
   143→                    info!("Connecting to server...");
   144→                    match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone(
   145→                        &ws_state,
   146→                    ))
   147→                    .await
   148→                    {
   149→                        Ok(_) => {
   150→                            warn!("WebSocket connection closed normally, reconnecting...");
   151→                        }
   152→                        Err(e) => {
   153→                            error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
   154→                        }
   155→                    }
   156→                    *ws_state.connected.write().await = false;
   157→                    tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
   158→                }
   159→            });
   160→
   161→            // Start metrics collection task
   162→            let metrics_state = std::sync::Arc::clone(&state);
   163→            let metrics_handle = tokio::spawn(async move {
   164→                let interval = metrics_state.config.metrics.interval_seconds;
   165→                let mut interval_timer =
   166→                    tokio::time::interval(tokio::time::Duration::from_secs(interval));
   167→
   168→                loop {
   169→                    interval_timer.tick().await;
   170→                    let metrics = metrics_state.metrics_collector.collect().await;
   171→                    if *metrics_state.connected.read().await {
   172→                        info!(
   173→                            "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
   174→                            metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
   175→                        );
   176→                    }
   177→                }
   178→            });
   179→
   180→            // Wait for shutdown signal from SCM
   181→            // We use a separate task to poll the channel since it's not async
   182→            let shutdown_handle = tokio::spawn(async move {
   183→                loop {
   184→                    match shutdown_rx.try_recv() {
   185→                        Ok(_) => {
   186→                            info!("Shutdown signal received");
   187→                            break;
   188→                        }
   189→                        Err(mpsc::TryRecvError::Empty) => {
   190→                            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
   191→                        }
   192→                        Err(mpsc::TryRecvError::Disconnected) => {
   193→                            warn!("Shutdown channel disconnected");
   194→                            break;
   195→                        }
   196→                    }
   197→                }
   198→            });
   199→
   200→            // Wait for shutdown
   201→            tokio::select! {
   202→                _ = shutdown_handle => {
   203→                    info!("Service shutting down gracefully");
   204→                }
   205→                _ = ws_handle => {
   206→                    error!("WebSocket task ended unexpectedly");
   207→                }
   208→                _ = metrics_handle => {
   209→                    error!("Metrics task ended unexpectedly");
   210→                }
   211→            }
   212→
   213→            Ok::<(), anyhow::Error>(())
   214→        });
   215→
   216→        // Report that we're stopping
   217→        status_handle
   218→            .set_service_status(ServiceStatus {
   219→                service_type: ServiceType::OWN_PROCESS,

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

            .context("Failed to set StartPending status")?;

        // Determine config path
        let config_path = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR));

        // Create the tokio runtime for the agent
        let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;

        // Start the agent in the runtime
        let agent_result = runtime.block_on(async {
            // Load configuration
            let config = match crate::config::AgentConfig::load(&config_path) {
                Ok(c) => c,
                Err(e) => {
                    error!("Failed to load config from {:?}: {}", config_path, e);
                    return Err(anyhow::anyhow!("Config load failed: {}", e));
                }
            };

            info!("GuruRMM Agent service starting...");
            info!("Config loaded from {:?}", config_path);
            info!("Server URL: {}", config.server.url);

            // Initialize metrics collector
            let metrics_collector = crate::metrics::MetricsCollector::new();
            info!("Metrics collector initialized");

            // Create shared state
            let state = std::sync::Arc::new(crate::AppState {
                config: config.clone(),
                metrics_collector,
                connected: tokio::sync::RwLock::new(false),
                last_checkin: tokio::sync::RwLock::new(None),
                tray_policy: tokio::sync::RwLock::new(crate::ipc::TrayPolicy::default_permissive()),
            });

            // Report that we're running
            status_handle
                .set_service_status(ServiceStatus {
                    service_type: ServiceType::OWN_PROCESS,
                    current_state: ServiceState::Running,
                    controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
                    exit_code: ServiceExitCode::Win32(0),
                    checkpoint: 0,
                    wait_hint: Duration::default(),
                    process_id: None,
                })
                .context("Failed to set Running status")?;

            // Start WebSocket client task
            let ws_state = std::sync::Arc::clone(&state);
            let ws_handle = tokio::spawn(async move {
                loop {
                    info!("Connecting to server...");
                    match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone(
                        &ws_state,
                    ))
                    .await
                    {
                        Ok(_) => {
                            warn!("WebSocket connection closed normally, reconnecting...");
                        }
                        Err(e) => {
                            error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
                        }
                    }
                    *ws_state.connected.write().await = false;
                    tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
                }
            });

            // Start metrics collection task
            let metrics_state = std::sync::Arc::clone(&state);
            let metrics_handle = tokio::spawn(async move {
                let interval = metrics_state.config.metrics.interval_seconds;
                let mut interval_timer =
                    tokio::time::interval(tokio::time::Duration::from_secs(interval));

                loop {
                    interval_timer.tick().await;
                    let metrics = metrics_state.metrics_collector.collect().await;
                    if *metrics_state.connected.read().await {
                        info!(
                            "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
                            metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
                        );
                    }
                }
            });

            // Wait for shutdown signal from SCM
            // We use a separate task to poll the channel since it's not async
            let shutdown_handle = tokio::spawn(async move {
                loop {
                    match shutdown_rx.try_recv() {
                        Ok(_) => {
                            info!("Shutdown signal received");
                            break;
                        }
                        Err(mpsc::TryRecvError::Empty) => {
                            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
                        }
                        Err(mpsc::TryRecvError::Disconnected) => {
                            warn!("Shutdown channel disconnected");
                            break;
                        }
                    }
                }
            });

            // Wait for shutdown
            tokio::select! {
                _ = shutdown_handle => {
                    info!("Service shutting down gracefully");
                }
                _ = ws_handle => {
                    error!("WebSocket task ended unexpectedly");
                }
                _ = metrics_handle => {
                    error!("Metrics task ended unexpectedly");
                }
            }

            Ok::<(), anyhow::Error>(())
        });

        // Report that we're stopping
        status_handle
            .set_service_status(ServiceStatus {
                service_type: ServiceType::OWN_PROCESS,


I see the service.rs file's run_service function. I need to add the tray launcher here as well. I'll add it after the metrics task and before the shutdown handling. I need to:

1. Add the tray launcher initialization
2. Add the tray termination on shutdown

Let me make the edit.