Write

//! Tray Launcher - Launches the tray application in user sessions
//!
//! This module handles launching the GuruRMM tray application in each
//! logged-in user's session. The agent service runs as SYSTEM, so we need
//! to use CreateProcessAsUser to launch the tray in the user's context.

#[cfg(windows)]
pub mod windows {
    use std::collections::HashMap;
    use std::path::PathBuf;
    use std::sync::Arc;
    use tokio::sync::RwLock;
    use tracing::{debug, error, info, warn};

    use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
    use windows::Win32::Security::{
        AdjustTokenPrivileges, DuplicateTokenEx, LookupPrivilegeValueW, SecurityIdentification,
        LUID_AND_ATTRIBUTES, SE_PRIVILEGE_ENABLED, TOKEN_ACCESS_MASK, TOKEN_ADJUST_PRIVILEGES,
        TOKEN_ALL_ACCESS, TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY,
        TokenPrimary,
    };
    use windows::Win32::System::RemoteDesktop::{
        WTSActive, WTSEnumerateSessionsW, WTSFreeMemory, WTSQueryUserToken, WTS_SESSION_INFOW,
    };
    use windows::Win32::System::Threading::{
        CreateProcessAsUserW, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTUPINFOW,
    };
    use windows::core::PCWSTR;

    use crate::ipc::TrayPolicy;

    /// Information about a running tray process
    #[derive(Debug)]
    struct TrayProcess {
        session_id: u32,
        process_handle: HANDLE,
        process_id: u32,
    }

    /// Manages tray application instances across user sessions
    pub struct TrayLauncher {
        /// Path to the tray executable
        tray_exe_path: PathBuf,

        /// Currently running tray processes (session_id -> process info)
        running: RwLock<HashMap<u32, TrayProcess>>,

        /// Current policy
        policy: RwLock<TrayPolicy>,
    }

    impl TrayLauncher {
        /// Create a new tray launcher
        pub fn new(tray_exe_path: PathBuf) -> Self {
            Self {
                tray_exe_path,
                running: RwLock::new(HashMap::new()),
                policy: RwLock::new(TrayPolicy::default_permissive()),
            }
        }

        /// Update the tray policy
        pub async fn update_policy(&self, policy: TrayPolicy) {
            let was_enabled = self.policy.read().await.enabled;
            *self.policy.write().await = policy.clone();

            if policy.enabled && !was_enabled {
                // Policy just enabled - launch in all sessions
                info!("Tray policy enabled, launching in user sessions");
                self.ensure_running().await;
            } else if !policy.enabled && was_enabled {
                // Policy just disabled - terminate all
                info!("Tray policy disabled, terminating tray processes");
                self.terminate_all().await;
            }
        }

        /// Ensure tray is running in all active user sessions
        pub async fn ensure_running(&self) {
            let policy = self.policy.read().await;
            if !policy.enabled {
                debug!("Tray policy disabled, skipping launch");
                return;
            }
            drop(policy);

            if !self.tray_exe_path.exists() {
                warn!("Tray executable not found: {:?}", self.tray_exe_path);
                return;
            }

            // Get active sessions
            let sessions = match self.enumerate_sessions() {
                Ok(s) => s,
                Err(e) => {
                    error!("Failed to enumerate sessions: {}", e);
                    return;
                }
            };

            let mut running = self.running.write().await;

            // Clean up dead processes
            running.retain(|session_id, proc| {
                if !is_process_running(proc.process_handle) {
                    debug!("Tray process in session {} has exited", session_id);
                    unsafe { let _ = CloseHandle(proc.process_handle); }
                    false
                } else {
                    true
                }
            });

            // Launch in sessions that don't have tray running
            for session_id in sessions {
                if running.contains_key(&session_id) {
                    continue;
                }

                match self.launch_in_session(session_id) {
                    Ok(proc) => {
                        info!(
                            "Launched tray in session {} (PID {})",
                            session_id, proc.process_id
                        );
                        running.insert(session_id, proc);
                    }
                    Err(e) => {
                        warn!("Failed to launch tray in session {}: {}", session_id, e);
                    }
                }
            }
        }

        /// Terminate all running tray processes
        pub async fn terminate_all(&self) {
            let mut running = self.running.write().await;

            for (session_id, proc) in running.drain() {
                info!("Terminating tray in session {} (PID {})", session_id, proc.process_id);
                terminate_process(proc.process_handle);
                unsafe { let _ = CloseHandle(proc.process_handle); }
            }
        }

        /// Handle a session change event
        pub async fn on_session_change(&self, session_id: u32, is_logon: bool) {
            if is_logon {
                info!("User logged on to session {}", session_id);
                // Small delay to let the session fully initialize
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
                self.ensure_running().await;
            } else {
                info!("User logged off from session {}", session_id);
                // Clean up the process entry (process likely already dead)
                let mut running = self.running.write().await;
                if let Some(proc) = running.remove(&session_id) {
                    unsafe { let _ = CloseHandle(proc.process_handle); }
                }
            }
        }

        /// Enumerate active user sessions
        fn enumerate_sessions(&self) -> anyhow::Result<Vec<u32>> {
            let mut sessions = Vec::new();

            unsafe {
                let mut session_info: *mut WTS_SESSION_INFOW = std::ptr::null_mut();
                let mut count: u32 = 0;

                // Enumerate sessions on the local server (None = local)
                let result = WTSEnumerateSessionsW(
                    None,
                    0,
                    1,
                    &mut session_info,
                    &mut count,
                );

                if !result.as_bool() {
                    return Err(anyhow::anyhow!(
                        "WTSEnumerateSessionsW failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Iterate through sessions
                let session_slice = std::slice::from_raw_parts(session_info, count as usize);
                for session in session_slice {
                    // Only consider active sessions (user logged in and at desktop)
                    if session.State == WTSActive && session.SessionId != 0 {
                        debug!(
                            "Found active session: ID={}, State={:?}",
                            session.SessionId, session.State
                        );
                        sessions.push(session.SessionId);
                    }
                }

                // Free the memory allocated by WTS
                WTSFreeMemory(session_info as *mut _);
            }

            Ok(sessions)
        }

        /// Launch the tray application in a specific user session
        fn launch_in_session(&self, session_id: u32) -> anyhow::Result<TrayProcess> {
            unsafe {
                // Get the user token for this session
                let mut user_token = HANDLE::default();
                if !WTSQueryUserToken(session_id, &mut user_token).as_bool() {
                    return Err(anyhow::anyhow!(
                        "WTSQueryUserToken failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Duplicate the token to get a primary token we can use
                let mut primary_token = HANDLE::default();
                let result = DuplicateTokenEx(
                    user_token,
                    TOKEN_ALL_ACCESS,
                    None,
                    SecurityIdentification,
                    TokenPrimary,
                    &mut primary_token,
                );

                // Close the original token
                let _ = CloseHandle(user_token);

                if !result.as_bool() {
                    return Err(anyhow::anyhow!(
                        "DuplicateTokenEx failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Build the command line
                let exe_path = self.tray_exe_path.to_string_lossy();
                let command_line: Vec<u16> = format!("\"{}\"", exe_path)
                    .encode_utf16()
                    .chain(std::iter::once(0))
                    .collect();

                // Set up startup info
                let mut startup_info = STARTUPINFOW::default();
                startup_info.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
                // Use the user's desktop
                let desktop: Vec<u16> = "winsta0\\default"
                    .encode_utf16()
                    .chain(std::iter::once(0))
                    .collect();
                startup_info.lpDesktop = windows::core::PWSTR(desktop.as_ptr() as *mut u16);

                let mut process_info = PROCESS_INFORMATION::default();

                // Create the process in the user's session
                let result = CreateProcessAsUserW(
                    primary_token,
                    None,
                    windows::core::PWSTR(command_line.as_ptr() as *mut u16),
                    None,
                    None,
                    false,
                    PROCESS_CREATION_FLAGS(0),
                    None,
                    None,
                    &startup_info,
                    &mut process_info,
                );

                // Close the duplicated token
                let _ = CloseHandle(primary_token);

                if !result.as_bool() {
                    return Err(anyhow::anyhow!(
                        "CreateProcessAsUserW failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Close the thread handle (we don't need it)
                let _ = CloseHandle(process_info.hThread);

                Ok(TrayProcess {
                    session_id,
                    process_handle: process_info.hProcess,
                    process_id: process_info.dwProcessId,
                })
            }
        }
    }

    impl Drop for TrayLauncher {
        fn drop(&mut self) {
            // Clean up process handles
            if let Ok(mut running) = self.running.try_write() {
                for (_, proc) in running.drain() {
                    unsafe { let _ = CloseHandle(proc.process_handle); }
                }
            }
        }
    }

    /// Check if a process is still running
    fn is_process_running(handle: HANDLE) -> bool {
        use windows::Win32::System::Threading::{GetExitCodeProcess, STILL_ACTIVE};

        unsafe {
            let mut exit_code: u32 = 0;
            if GetExitCodeProcess(handle, &mut exit_code).as_bool() {
                exit_code == STILL_ACTIVE.0 as u32
            } else {
                false
            }
        }
    }

    /// Terminate a process
    fn terminate_process(handle: HANDLE) {
        use windows::Win32::System::Threading::TerminateProcess;

        unsafe {
            let _ = TerminateProcess(handle, 0);
        }
    }
}

#[cfg(unix)]
pub mod unix {
    use std::path::PathBuf;
    use tracing::info;

    use crate::ipc::TrayPolicy;

    /// Placeholder for Unix tray launcher
    pub struct TrayLauncher {
        tray_exe_path: PathBuf,
    }

    impl TrayLauncher {
        pub fn new(tray_exe_path: PathBuf) -> Self {
            Self { tray_exe_path }
        }

        pub async fn update_policy(&self, _policy: TrayPolicy) {
            // TODO: Implement for Unix (XDG autostart or user systemd service)
            info!("Unix tray launcher not yet implemented");
        }

        pub async fn ensure_running(&self) {
            info!("Unix tray launcher not yet implemented");
        }

        pub async fn terminate_all(&self) {
            info!("Unix tray launcher not yet implemented");
        }

        pub async fn on_session_change(&self, _session_id: u32, _is_logon: bool) {
            // No-op on Unix for now
        }
    }
}

#[cfg(windows)]
pub use windows::TrayLauncher;

#[cfg(unix)]
pub use unix::TrayLauncher;


File created successfully at: C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\tray_launcher.rs

//! Tray Launcher - Launches the tray application in user sessions
//!
//! This module handles launching the GuruRMM tray application in each
//! logged-in user's session. The agent service runs as SYSTEM, so we need
//! to use CreateProcessAsUser to launch the tray in the user's context.

#[cfg(windows)]
pub mod windows {
    use std::collections::HashMap;
    use std::path::PathBuf;
    use std::sync::Arc;
    use tokio::sync::RwLock;
    use tracing::{debug, error, info, warn};

    use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
    use windows::Win32::Security::{
        AdjustTokenPrivileges, DuplicateTokenEx, LookupPrivilegeValueW, SecurityIdentification,
        LUID_AND_ATTRIBUTES, SE_PRIVILEGE_ENABLED, TOKEN_ACCESS_MASK, TOKEN_ADJUST_PRIVILEGES,
        TOKEN_ALL_ACCESS, TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY,
        TokenPrimary,
    };
    use windows::Win32::System::RemoteDesktop::{
        WTSActive, WTSEnumerateSessionsW, WTSFreeMemory, WTSQueryUserToken, WTS_SESSION_INFOW,
    };
    use windows::Win32::System::Threading::{
        CreateProcessAsUserW, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTUPINFOW,
    };
    use windows::core::PCWSTR;

    use crate::ipc::TrayPolicy;

    /// Information about a running tray process
    #[derive(Debug)]
    struct TrayProcess {
        session_id: u32,
        process_handle: HANDLE,
        process_id: u32,
    }

    /// Manages tray application instances across user sessions
    pub struct TrayLauncher {
        /// Path to the tray executable
        tray_exe_path: PathBuf,

        /// Currently running tray processes (session_id -> process info)
        running: RwLock<HashMap<u32, TrayProcess>>,

        /// Current policy
        policy: RwLock<TrayPolicy>,
    }

    impl TrayLauncher {
        /// Create a new tray launcher
        pub fn new(tray_exe_path: PathBuf) -> Self {
            Self {
                tray_exe_path,
                running: RwLock::new(HashMap::new()),
                policy: RwLock::new(TrayPolicy::default_permissive()),
            }
        }

        /// Update the tray policy
        pub async fn update_policy(&self, policy: TrayPolicy) {
            let was_enabled = self.policy.read().await.enabled;
            *self.policy.write().await = policy.clone();

            if policy.enabled && !was_enabled {
                // Policy just enabled - launch in all sessions
                info!("Tray policy enabled, launching in user sessions");
                self.ensure_running().await;
            } else if !policy.enabled && was_enabled {
                // Policy just disabled - terminate all
                info!("Tray policy disabled, terminating tray processes");
                self.terminate_all().await;
            }
        }

        /// Ensure tray is running in all active user sessions
        pub async fn ensure_running(&self) {
            let policy = self.policy.read().await;
            if !policy.enabled {
                debug!("Tray policy disabled, skipping launch");
                return;
            }
            drop(policy);

            if !self.tray_exe_path.exists() {
                warn!("Tray executable not found: {:?}", self.tray_exe_path);
                return;
            }

            // Get active sessions
            let sessions = match self.enumerate_sessions() {
                Ok(s) => s,
                Err(e) => {
                    error!("Failed to enumerate sessions: {}", e);
                    return;
                }
            };

            let mut running = self.running.write().await;

            // Clean up dead processes
            running.retain(|session_id, proc| {
                if !is_process_running(proc.process_handle) {
                    debug!("Tray process in session {} has exited", session_id);
                    unsafe { let _ = CloseHandle(proc.process_handle); }
                    false
                } else {
                    true
                }
            });

            // Launch in sessions that don't have tray running
            for session_id in sessions {
                if running.contains_key(&session_id) {
                    continue;
                }

                match self.launch_in_session(session_id) {
                    Ok(proc) => {
                        info!(
                            "Launched tray in session {} (PID {})",
                            session_id, proc.process_id
                        );
                        running.insert(session_id, proc);
                    }
                    Err(e) => {
                        warn!("Failed to launch tray in session {}: {}", session_id, e);
                    }
                }
            }
        }

        /// Terminate all running tray processes
        pub async fn terminate_all(&self) {
            let mut running = self.running.write().await;

            for (session_id, proc) in running.drain() {
                info!("Terminating tray in session {} (PID {})", session_id, proc.process_id);
                terminate_process(proc.process_handle);
                unsafe { let _ = CloseHandle(proc.process_handle); }
            }
        }

        /// Handle a session change event
        pub async fn on_session_change(&self, session_id: u32, is_logon: bool) {
            if is_logon {
                info!("User logged on to session {}", session_id);
                // Small delay to let the session fully initialize
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
                self.ensure_running().await;
            } else {
                info!("User logged off from session {}", session_id);
                // Clean up the process entry (process likely already dead)
                let mut running = self.running.write().await;
                if let Some(proc) = running.remove(&session_id) {
                    unsafe { let _ = CloseHandle(proc.process_handle); }
                }
            }
        }

        /// Enumerate active user sessions
        fn enumerate_sessions(&self) -> anyhow::Result<Vec<u32>> {
            let mut sessions = Vec::new();

            unsafe {
                let mut session_info: *mut WTS_SESSION_INFOW = std::ptr::null_mut();
                let mut count: u32 = 0;

                // Enumerate sessions on the local server (None = local)
                let result = WTSEnumerateSessionsW(
                    None,
                    0,
                    1,
                    &mut session_info,
                    &mut count,
                );

                if !result.as_bool() {
                    return Err(anyhow::anyhow!(
                        "WTSEnumerateSessionsW failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Iterate through sessions
                let session_slice = std::slice::from_raw_parts(session_info, count as usize);
                for session in session_slice {
                    // Only consider active sessions (user logged in and at desktop)
                    if session.State == WTSActive && session.SessionId != 0 {
                        debug!(
                            "Found active session: ID={}, State={:?}",
                            session.SessionId, session.State
                        );
                        sessions.push(session.SessionId);
                    }
                }

                // Free the memory allocated by WTS
                WTSFreeMemory(session_info as *mut _);
            }

            Ok(sessions)
        }

        /// Launch the tray application in a specific user session
        fn launch_in_session(&self, session_id: u32) -> anyhow::Result<TrayProcess> {
            unsafe {
                // Get the user token for this session
                let mut user_token = HANDLE::default();
                if !WTSQueryUserToken(session_id, &mut user_token).as_bool() {
                    return Err(anyhow::anyhow!(
                        "WTSQueryUserToken failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Duplicate the token to get a primary token we can use
                let mut primary_token = HANDLE::default();
                let result = DuplicateTokenEx(
                    user_token,
                    TOKEN_ALL_ACCESS,
                    None,
                    SecurityIdentification,
                    TokenPrimary,
                    &mut primary_token,
                );

                // Close the original token
                let _ = CloseHandle(user_token);

                if !result.as_bool() {
                    return Err(anyhow::anyhow!(
                        "DuplicateTokenEx failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Build the command line
                let exe_path = self.tray_exe_path.to_string_lossy();
                let command_line: Vec<u16> = format!("\"{}\"", exe_path)
                    .encode_utf16()
                    .chain(std::iter::once(0))
                    .collect();

                // Set up startup info
                let mut startup_info = STARTUPINFOW::default();
                startup_info.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
                // Use the user's desktop
                let desktop: Vec<u16> = "winsta0\\default"
                    .encode_utf16()
                    .chain(std::iter::once(0))
                    .collect();
                startup_info.lpDesktop = windows::core::PWSTR(desktop.as_ptr() as *mut u16);

                let mut process_info = PROCESS_INFORMATION::default();

                // Create the process in the user's session
                let result = CreateProcessAsUserW(
                    primary_token,
                    None,
                    windows::core::PWSTR(command_line.as_ptr() as *mut u16),
                    None,
                    None,
                    false,
                    PROCESS_CREATION_FLAGS(0),
                    None,
                    None,
                    &startup_info,
                    &mut process_info,
                );

                // Close the duplicated token
                let _ = CloseHandle(primary_token);

                if !result.as_bool() {
                    return Err(anyhow::anyhow!(
                        "CreateProcessAsUserW failed: {:?}",
                        windows::core::Error::from_win32()
                    ));
                }

                // Close the thread handle (we don't need it)
                let _ = CloseHandle(process_info.hThread);

                Ok(TrayProcess {
                    session_id,
                    process_handle: process_info.hProcess,
                    process_id: process_info.dwProcessId,
                })
            }
        }
    }

    impl Drop for TrayLauncher {
        fn drop(&mut self) {
            // Clean up process handles
            if let Ok(mut running) = self.running.try_write() {
                for (_, proc) in running.drain() {
                    unsafe { let _ = CloseHandle(proc.process_handle); }
                }
            }
        }
    }

    /// Check if a process is still running
    fn is_process_running(handle: HANDLE) -> bool {
        use windows::Win32::System::Threading::{GetExitCodeProcess, STILL_ACTIVE};

        unsafe {
            let mut exit_code: u32 = 0;
            if GetExitCodeProcess(handle, &mut exit_code).as_bool() {
                exit_code == STILL_ACTIVE.0 as u32
            } else {
                false
            }
        }
    }

    /// Terminate a process
    fn terminate_process(handle: HANDLE) {
        use windows::Win32::System::Threading::TerminateProcess;

        unsafe {
            let _ = TerminateProcess(handle, 0);
        }
    }
}

#[cfg(unix)]
pub mod unix {
    use std::path::PathBuf;
    use tracing::info;

    use crate::ipc::TrayPolicy;

    /// Placeholder for Unix tray launcher
    pub struct TrayLauncher {
        tray_exe_path: PathBuf,
    }

    impl TrayLauncher {
        pub fn new(tray_exe_path: PathBuf) -> Self {
            Self { tray_exe_path }
        }

        pub async fn update_policy(&self, _policy: TrayPolicy) {
            // TODO: Implement for Unix (XDG autostart or user systemd service)
            info!("Unix tray launcher not yet implemented");
        }

        pub async fn ensure_running(&self) {
            info!("Unix tray launcher not yet implemented");
        }

        pub async fn terminate_all(&self) {
            info!("Unix tray launcher not yet implemented");
        }

        pub async fn on_session_change(&self, _session_id: u32, _is_logon: bool) {
            // No-op on Unix for now
        }
    }
}

#[cfg(windows)]
pub use windows::TrayLauncher;

#[cfg(unix)]
pub use unix::TrayLauncher;


Now I need to:
1. Add the module to main.rs
2. Integrate the tray launcher into the agent's main loop
3. Update the install command to copy the tray binary too

Let me update main.rs to add the module declaration and integrate the tray launcher.


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

Update install command to bundle/copy tray binary

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

Test tray launcher functionality


Now I need to add the module to main.rs and integrate the tray launcher. Let me read main.rs first to understand the structure better.


     1→//! GuruRMM Agent - Cross-platform Remote Monitoring and Management Agent
     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 updater;
    13→
    14→use anyhow::{Context, Result};
    15→use clap::{Parser, Subcommand};
    16→use std::path::PathBuf;
    17→use std::sync::Arc;
    18→use tokio::sync::RwLock;
    19→use tracing::{error, info, warn};
    20→
    21→use crate::config::AgentConfig;
    22→use crate::metrics::MetricsCollector;
    23→use crate::transport::WebSocketClient;
    24→
    25→/// GuruRMM Agent - Remote Monitoring and Management
    26→#[derive(Parser)]
    27→#[command(name = "gururmm-agent")]
    28→#[command(author, version, about, long_about = None)]
    29→struct Cli {
    30→    /// Path to configuration file
    31→    #[arg(short, long, default_value = "agent.toml")]
    32→    config: PathBuf,
    33→
    34→    /// Subcommand to run
    35→    #[command(subcommand)]
    36→    command: Option<Commands>,
    37→}
    38→
    39→#[derive(Subcommand)]
    40→enum Commands {
    41→    /// Run the agent (default)
    42→    Run,
    43→
    44→    /// Install as a system service
    45→    Install {
    46→        /// Server WebSocket URL (e.g., wss://rmm-api.example.com/ws)
    47→        #[arg(long)]
    48→        server_url: Option<String>,
    49→
    50→        /// API key for authentication
    51→        #[arg(long)]
    52→        api_key: Option<String>,
    53→
    54→        /// Skip legacy service detection and cleanup
    55→        #[arg(long, default_value = "false")]
    56→        skip_legacy_check: bool,
    57→    },
    58→
    59→    /// Uninstall the system service
    60→    Uninstall,
    61→
    62→    /// Start the installed service
    63→    Start,
    64→
    65→    /// Stop the installed service
    66→    Stop,
    67→
    68→    /// Show agent status
    69→    Status,
    70→
    71→    /// Generate a sample configuration file
    72→    GenerateConfig {
    73→        /// Output path for config file
    74→        #[arg(short, long, default_value = "agent.toml")]
    75→        output: PathBuf,
    76→    },
    77→
    78→    /// Run as Windows service (called by SCM, not for manual use)
    79→    #[command(hide = true)]
    80→    Service,
    81→}
    82→
    83→/// Shared application state
    84→pub struct AppState {
    85→    pub config: AgentConfig,
    86→    pub metrics_collector: MetricsCollector,
    87→    pub connected: RwLock<bool>,
    88→    pub last_checkin: RwLock<Option<chrono::DateTime<chrono::Utc>>>,
    89→    pub tray_policy: RwLock<ipc::TrayPolicy>,
    90→}
    91→
    92→#[tokio::main]
    93→async fn main() -> Result<()> {
    94→    // Initialize logging
    95→    tracing_subscriber::fmt()
    96→        .with_env_filter(
    97→            tracing_subscriber::EnvFilter::from_default_env()
    98→                .add_directive("gururmm_agent=info".parse()?)
    99→                .add_directive("info".parse()?),
   100→        )
   101→        .init();
   102→
   103→    let cli = Cli::parse();
   104→
   105→    match cli.command.unwrap_or(Commands::Run) {
   106→        Commands::Run => run_agent(cli.config).await,
   107→        Commands::Install { server_url, api_key, skip_legacy_check } => {
   108→            install_service(server_url, api_key, skip_legacy_check).await
   109→        }
   110→        Commands::Uninstall => uninstall_service().await,
   111→        Commands::Start => start_service().await,
   112→        Commands::Stop => stop_service().await,
   113→        Commands::Status => show_status(cli.config).await,
   114→        Commands::GenerateConfig { output } => generate_config(output).await,
   115→        Commands::Service => run_as_windows_service(),
   116→    }
   117→}
   118→
   119→/// Run as a Windows service (called by SCM)
   120→fn run_as_windows_service() -> Result<()> {
   121→    #[cfg(windows)]
   122→    {
   123→        service::windows::run_as_service()
   124→    }
   125→
   126→    #[cfg(not(windows))]
   127→    {
   128→        anyhow::bail!("Windows service mode is only available on Windows");
   129→    }
   130→}
   131→
   132→/// Main agent runtime loop
   133→async fn run_agent(config_path: PathBuf) -> Result<()> {
   134→    info!("GuruRMM Agent starting...");
   135→
   136→    // Load configuration
   137→    let config = AgentConfig::load(&config_path)?;
   138→    info!("Loaded configuration from {:?}", config_path);
   139→    info!("Server URL: {}", config.server.url);
   140→
   141→    // Initialize metrics collector
   142→    let metrics_collector = MetricsCollector::new();
   143→    info!("Metrics collector initialized");
   144→
   145→    // Create channels for IPC commands
   146→    let (force_checkin_tx, mut force_checkin_rx) = tokio::sync::mpsc::channel::<()>(8);
   147→    let (stop_agent_tx, mut stop_agent_rx) = tokio::sync::mpsc::channel::<()>(1);
   148→
   149→    // Create shared state
   150→    let state = Arc::new(AppState {
   151→        config: config.clone(),
   152→        metrics_collector,
   153→        connected: RwLock::new(false),
   154→        last_checkin: RwLock::new(None),
   155→        tray_policy: RwLock::new(ipc::TrayPolicy::default_permissive()),
   156→    });
   157→
   158→    // Create IPC state for tray communication
   159→    let device_id = device_id::get_device_id();
   160→    let hostname = hostname::get()
   161→        .map(|h| h.to_string_lossy().to_string())
   162→        .unwrap_or_else(|_| "unknown".to_string());
   163→
   164→    let initial_status = ipc::AgentStatus {
   165→        connected: false,
   166→        last_checkin: None,
   167→        server_url: config.server.url.clone(),
   168→        agent_version: env!("CARGO_PKG_VERSION").to_string(),
   169→        device_id,
   170→        hostname,
   171→        error: None,
   172→    };
   173→
   174→    let ipc_state = Arc::new(ipc::IpcState::new(
   175→        initial_status,
   176→        ipc::TrayPolicy::default_permissive(),
   177→        force_checkin_tx,
   178→        stop_agent_tx,
   179→    ));
   180→
   181→    // Start IPC server for tray application
   182→    let ipc_server_state = Arc::clone(&ipc_state);
   183→    let ipc_handle = tokio::spawn(async move {
   184→        if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
   185→            error!("IPC server error: {}", e);
   186→        }
   187→    });
   188→    info!("IPC server started");
   189→
   190→    // Start the WebSocket client with auto-reconnect
   191→    let ws_state = Arc::clone(&state);
   192→    let ws_handle = tokio::spawn(async move {
   193→        loop {
   194→            info!("Connecting to server...");
   195→            match WebSocketClient::connect_and_run(Arc::clone(&ws_state)).await {
   196→                Ok(_) => {
   197→                    warn!("WebSocket connection closed normally, reconnecting...");
   198→                }
   199→                Err(e) => {
   200→                    error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
   201→                }
   202→            }
   203→
   204→            // Mark as disconnected
   205→            *ws_state.connected.write().await = false;
   206→
   207→            // Wait before reconnecting
   208→            tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
   209→        }
   210→    });
   211→
   212→    // Start metrics collection loop
   213→    let metrics_state = Arc::clone(&state);
   214→    let metrics_handle = tokio::spawn(async move {
   215→        let interval = metrics_state.config.metrics.interval_seconds;
   216→        let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval));
   217→
   218→        loop {
   219→            interval_timer.tick().await;
   220→
   221→            // Collect metrics (they'll be sent via WebSocket if connected)
   222→            let metrics = metrics_state.metrics_collector.collect().await;
   223→            if *metrics_state.connected.read().await {
   224→                info!(
   225→                    "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
   226→                    metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
   227→                );
   228→            }
   229→        }
   230→    });
   231→
   232→    // Task to update IPC status periodically
   233→    let ipc_update_state = Arc::clone(&state);
   234→    let ipc_update_ipc = Arc::clone(&ipc_state);
   235→    let ipc_update_handle = tokio::spawn(async move {
   236→        let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
   237→        loop {
   238→            interval.tick().await;
   239→
   240→            let connected = *ipc_update_state.connected.read().await;
   241→            let last_checkin = ipc_update_state.last_checkin.read().await.map(|dt| dt.to_rfc3339());
   242→
   243→            let status = ipc::AgentStatus {
   244→                connected,
   245→                last_checkin,
   246→                server_url: ipc_update_state.config.server.url.clone(),
   247→                agent_version: env!("CARGO_PKG_VERSION").to_string(),
   248→                device_id: device_id::get_device_id(),
   249→                hostname: hostname::get()
   250→                    .map(|h| h.to_string_lossy().to_string())
   251→                    .unwrap_or_else(|_| "unknown".to_string()),
   252→                error: if connected { None } else { Some("Disconnected".to_string()) },
   253→            };
   254→
   255→            ipc_update_ipc.update_status(status).await;
   256→        }
   257→    });
   258→
   259→    // Wait for shutdown signal or stop request
   260→    tokio::select! {
   261→        _ = tokio::signal::ctrl_c() => {
   262→            info!("Received shutdown signal");
   263→        }
   264→        _ = stop_agent_rx.recv() => {
   265→            info!("Received stop request from tray");
   266→        }
   267→        _ = ws_handle => {
   268→            error!("WebSocket task ended unexpectedly");
   269→        }
   270→        _ = metrics_handle => {
   271→            error!("Metrics task ended unexpectedly");
   272→        }
   273→        _ = ipc_handle => {
   274→            error!("IPC server ended unexpectedly");
   275→        }
   276→        _ = ipc_update_handle => {
   277→            error!("IPC update task ended unexpectedly");
   278→        }
   279→    }
   280→
   281→    info!("GuruRMM Agent shutting down");
   282→    Ok(())
   283→}
   284→
   285→/// Install the agent as a system service
   286→async fn install_service(
   287→    server_url: Option<String>,
   288→    api_key: Option<String>,
   289→    skip_legacy_check: bool,
   290→) -> Result<()> {
   291→    #[cfg(windows)]
   292→    {
   293→        service::windows::install(server_url, api_key, skip_legacy_check)
   294→    }
   295→
   296→    #[cfg(target_os = "linux")]
   297→    {
   298→        install_systemd_service(server_url, api_key, skip_legacy_check).await
   299→    }
   300→
   301→    #[cfg(target_os = "macos")]
   302→    {
   303→        let _ = (server_url, api_key, skip_legacy_check); // Suppress unused warnings
   304→        info!("Installing GuruRMM Agent as launchd service...");
   305→        todo!("macOS launchd service installation not yet implemented");
   306→    }
   307→}
   308→
   309→/// Legacy service names to check for and clean up (Linux)
   310→#[cfg(target_os = "linux")]
   311→const LINUX_LEGACY_SERVICE_NAMES: &[&str] = &[
   312→    "gururmm",           // Old name without -agent suffix
   313→    "guru-rmm-agent",    // Alternative naming
   314→    "GuruRMM-Agent",     // Case variant
   315→];
   316→
   317→/// Clean up legacy Linux service installations
   318→#[cfg(target_os = "linux")]
   319→fn cleanup_legacy_linux_services() -> Result<()> {
   320→    use std::process::Command;
   321→
   322→    info!("Checking for legacy service installations...");
   323→
   324→    for legacy_name in LINUX_LEGACY_SERVICE_NAMES {
   325→        // Check if service exists
   326→        let status = Command::new("systemctl")
   327→            .args(["status", legacy_name])
   328→            .output();
   329→
   330→        if let Ok(output) = status {
   331→            if output.status.success() || String::from_utf8_lossy(&output.stderr).contains("Loaded:") {
   332→                info!("Found legacy service '{}', removing...", legacy_name);
   333→
   334→                // Stop the service
   335→                let _ = Command::new("systemctl")
   336→                    .args(["stop", legacy_name])
   337→                    .status();
   338→
   339→                // Disable the service
   340→                let _ = Command::new("systemctl")
   341→                    .args(["disable", legacy_name])
   342→                    .status();
   343→
   344→                // Remove unit file
   345→                let unit_file = format!("/etc/systemd/system/{}.service", legacy_name);
   346→                if std::path::Path::new(&unit_file).exists() {
   347→                    info!("Removing legacy unit file: {}", unit_file);
   348→                    let _ = std::fs::remove_file(&unit_file);
   349→                }
   350→            }
   351→        }
   352→    }
   353→
   354→    // Check for legacy binaries in common locations
   355→    let legacy_binary_locations = [
   356→        "/usr/local/bin/gururmm",
   357→        "/usr/bin/gururmm",
   358→        "/opt/gururmm/gururmm",
   359→        "/opt/gururmm/agent",
   360→    ];
   361→
   362→    for legacy_path in legacy_binary_locations {
   363→        if std::path::Path::new(legacy_path).exists() {
   364→            info!("Found legacy binary at '{}', removing...", legacy_path);
   365→            let _ = std::fs::remove_file(legacy_path);
   366→        }
   367→    }
   368→
   369→    // Reload systemd to pick up removed unit files
   370→    let _ = Command::new("systemctl")
   371→        .args(["daemon-reload"])
   372→        .status();
   373→
   374→    Ok(())
   375→}
   376→
   377→/// Install as a systemd service (Linux)
   378→#[cfg(target_os = "linux")]
   379→async fn install_systemd_service(
   380→    server_url: Option<String>,
   381→    api_key: Option<String>,
   382→    skip_legacy_check: bool,
   383→) -> Result<()> {
   384→    use std::process::Command;
   385→
   386→    const SERVICE_NAME: &str = "gururmm-agent";
   387→    const INSTALL_DIR: &str = "/usr/local/bin";
   388→    const CONFIG_DIR: &str = "/etc/gururmm";
   389→    const SYSTEMD_DIR: &str = "/etc/systemd/system";
   390→
   391→    info!("Installing GuruRMM Agent as systemd service...");
   392→
   393→    // Check if running as root
   394→    if !nix::unistd::geteuid().is_root() {
   395→        anyhow::bail!("Installation requires root privileges. Please run with sudo.");
   396→    }
   397→
   398→    // Clean up legacy installations unless skipped
   399→    if !skip_legacy_check {
   400→        if let Err(e) = cleanup_legacy_linux_services() {
   401→            warn!("Legacy cleanup warning: {}", e);
   402→        }
   403→    }
   404→
   405→    // Get the current executable path
   406→    let current_exe = std::env::current_exe()
   407→        .context("Failed to get current executable path")?;
   408→
   409→    let binary_dest = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
   410→    let config_dest = format!("{}/agent.toml", CONFIG_DIR);
   411→    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);
   412→
   413→    // Create config directory
   414→    info!("Creating config directory: {}", CONFIG_DIR);
   415→    std::fs::create_dir_all(CONFIG_DIR)
   416→        .context("Failed to create config directory")?;
   417→
   418→    // Copy binary
   419→    info!("Copying binary to: {}", binary_dest);
   420→    std::fs::copy(&current_exe, &binary_dest)
   421→        .context("Failed to copy binary")?;
   422→
   423→    // Make binary executable
   424→    Command::new("chmod")
   425→        .args(["+x", &binary_dest])
   426→        .status()
   427→        .context("Failed to set binary permissions")?;
   428→
   429→    // Handle configuration
   430→    let config_needs_manual_edit;
   431→    if !std::path::Path::new(&config_dest).exists() {
   432→        info!("Creating config: {}", config_dest);
   433→
   434→        // Start with sample config
   435→        let mut config = crate::config::AgentConfig::sample();
   436→
   437→        // Apply provided values
   438→        if let Some(url) = &server_url {
   439→            config.server.url = url.clone();
   440→        }
   441→        if let Some(key) = &api_key {
   442→            config.server.api_key = key.clone();
   443→        }
   444→
   445→        let toml_str = toml::to_string_pretty(&config)?;
   446→        std::fs::write(&config_dest, toml_str)
   447→            .context("Failed to write config file")?;
   448→
   449→        // Set restrictive permissions on config (contains API key)
   450→        Command::new("chmod")
   451→            .args(["600", &config_dest])
   452→            .status()
   453→            .context("Failed to set config permissions")?;
   454→
   455→        config_needs_manual_edit = server_url.is_none() || api_key.is_none();
   456→    } else {
   457→        info!("Config already exists: {}", config_dest);
   458→        config_needs_manual_edit = false;
   459→
   460→        // If server_url or api_key provided, update existing config
   461→        if server_url.is_some() || api_key.is_some() {
   462→            info!("Updating existing configuration...");
   463→            let config_content = std::fs::read_to_string(&config_dest)?;
   464→            let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
   465→                .context("Failed to parse existing config")?;
   466→
   467→            if let Some(url) = &server_url {
   468→                config.server.url = url.clone();
   469→            }
   470→            if let Some(key) = &api_key {
   471→                config.server.api_key = key.clone();
   472→            }
   473→
   474→            let toml_str = toml::to_string_pretty(&config)?;
   475→            std::fs::write(&config_dest, toml_str)
   476→                .context("Failed to update config file")?;
   477→        }
   478→    }
   479→
   480→    // Create systemd unit file
   481→    let unit_content = format!(r#"[Unit]
   482→Description=GuruRMM Agent - Remote Monitoring and Management
   483→Documentation=https://github.com/azcomputerguru/gururmm
   484→After=network-online.target
   485→Wants=network-online.target
   486→
   487→[Service]
   488→Type=simple
   489→ExecStart={binary} --config {config} run
   490→Restart=always
   491→RestartSec=10
   492→StandardOutput=journal
   493→StandardError=journal
   494→SyslogIdentifier={service}
   495→
   496→# Security hardening
   497→NoNewPrivileges=true
   498→ProtectSystem=strict
   499→ProtectHome=read-only
   500→PrivateTmp=true
   501→ReadWritePaths=/var/log
   502→
   503→[Install]
   504→WantedBy=multi-user.target
   505→"#,
   506→        binary = binary_dest,
   507→        config = config_dest,
   508→        service = SERVICE_NAME
   509→    );
   510→
   511→    info!("Creating systemd unit file: {}", unit_file);
   512→    std::fs::write(&unit_file, unit_content)
   513→        .context("Failed to write systemd unit file")?;
   514→
   515→    // Reload systemd daemon
   516→    info!("Reloading systemd daemon...");
   517→    let status = Command::new("systemctl")
   518→        .args(["daemon-reload"])
   519→        .status()
   520→        .context("Failed to reload systemd")?;
   521→
   522→    if !status.success() {
   523→        anyhow::bail!("systemctl daemon-reload failed");
   524→    }
   525→
   526→    // Enable the service
   527→    info!("Enabling service...");
   528→    let status = Command::new("systemctl")
   529→        .args(["enable", SERVICE_NAME])
   530→        .status()
   531→        .context("Failed to enable service")?;
   532→
   533→    if !status.success() {
   534→        anyhow::bail!("systemctl enable failed");
   535→    }
   536→
   537→    println!("\n✓ GuruRMM Agent installed successfully!");
   538→    println!("\nInstalled files:");
   539→    println!("  Binary: {}", binary_dest);
   540→    println!("  Config: {}", config_dest);
   541→    println!("  Service: {}", unit_file);
   542→
   543→    if config_needs_manual_edit {
   544→        println!("\n⚠  IMPORTANT: Edit {} with your server URL and API key!", config_dest);
   545→        println!("\nNext steps:");
   546→        println!("  1. Edit {} with your server URL and API key", config_dest);
   547→        println!("  2. Start the service: sudo systemctl start {}", SERVICE_NAME);
   548→    } else {
   549→        println!("\nStarting service...");
   550→        let status = Command::new("systemctl")
   551→            .args(["start", SERVICE_NAME])
   552→            .status();
   553→
   554→        if status.is_ok() && status.unwrap().success() {
   555→            println!("✓ Service started successfully!");
   556→        } else {
   557→            println!("⚠  Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
   558→        }
   559→    }
   560→
   561→    println!("\nUseful commands:");
   562→    println!("  Status: sudo systemctl status {}", SERVICE_NAME);
   563→    println!("  Logs:   sudo journalctl -u {} -f", SERVICE_NAME);
   564→    println!("  Stop:   sudo systemctl stop {}", SERVICE_NAME);
   565→    println!("  Start:  sudo systemctl start {}", SERVICE_NAME);
   566→
   567→    Ok(())
   568→}
   569→
   570→/// Uninstall the system service
   571→async fn uninstall_service() -> Result<()> {
   572→    #[cfg(windows)]
   573→    {
   574→        service::windows::uninstall()
   575→    }
   576→
   577→    #[cfg(target_os = "linux")]
   578→    {
   579→        uninstall_systemd_service().await
   580→    }
   581→
   582→    #[cfg(target_os = "macos")]
   583→    {
   584→        todo!("macOS service uninstallation not yet implemented");
   585→    }
   586→}
   587→
   588→/// Uninstall systemd service (Linux)
   589→#[cfg(target_os = "linux")]
   590→async fn uninstall_systemd_service() -> Result<()> {
   591→    use std::process::Command;
   592→
   593→    const SERVICE_NAME: &str = "gururmm-agent";
   594→    const INSTALL_DIR: &str = "/usr/local/bin";
   595→    const CONFIG_DIR: &str = "/etc/gururmm";
   596→    const SYSTEMD_DIR: &str = "/etc/systemd/system";
   597→
   598→    info!("Uninstalling GuruRMM Agent...");
   599→
   600→    if !nix::unistd::geteuid().is_root() {
   601→        anyhow::bail!("Uninstallation requires root privileges. Please run with sudo.");
   602→    }
   603→
   604→    let binary_path = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
   605→    let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);
   606→
   607→    // Stop the service if running
   608→    info!("Stopping service...");
   609→    let _ = Command::new("systemctl")
   610→        .args(["stop", SERVICE_NAME])
   611→        .status();
   612→
   613→    // Disable the service
   614→    info!("Disabling service...");
   615→    let _ = Command::new("systemctl")
   616→        .args(["disable", SERVICE_NAME])
   617→        .status();
   618→
   619→    // Remove unit file
   620→    if std::path::Path::new(&unit_file).exists() {
   621→        info!("Removing unit file: {}", unit_file);
   622→        std::fs::remove_file(&unit_file)?;
   623→    }
   624→
   625→    // Remove binary
   626→    if std::path::Path::new(&binary_path).exists() {
   627→        info!("Removing binary: {}", binary_path);
   628→        std::fs::remove_file(&binary_path)?;
   629→    }
   630→
   631→    // Reload systemd
   632→    let _ = Command::new("systemctl")
   633→        .args(["daemon-reload"])
   634→        .status();
   635→
   636→    println!("\n✓ GuruRMM Agent uninstalled successfully!");
   637→    println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
   638→    println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);
   639→
   640→    Ok(())
   641→}
   642→
   643→/// Start the installed service
   644→async fn start_service() -> Result<()> {
   645→    #[cfg(windows)]
   646→    {
   647→        service::windows::start()
   648→    }
   649→
   650→    #[cfg(target_os = "linux")]
   651→    {
   652→        use std::process::Command;
   653→
   654→        info!("Starting GuruRMM Agent service...");
   655→
   656→        let status = Command::new("systemctl")
   657→            .args(["start", "gururmm-agent"])
   658→            .status()
   659→            .context("Failed to start service")?;
   660→
   661→        if status.success() {
   662→            println!("** Service started successfully");
   663→            println!("Check status: sudo systemctl status gururmm-agent");
   664→        } else {
   665→            anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
   666→        }
   667→
   668→        Ok(())
   669→    }
   670→
   671→    #[cfg(target_os = "macos")]
   672→    {
   673→        todo!("macOS service start not yet implemented");
   674→    }
   675→}
   676→
   677→/// Stop the installed service
   678→async fn stop_service() -> Result<()> {
   679→    #[cfg(windows)]
   680→    {
   681→        service::windows::stop()
   682→    }
   683→
   684→    #[cfg(target_os = "linux")]
   685→    {
   686→        use std::process::Command;
   687→
   688→        info!("Stopping GuruRMM Agent service...");
   689→
   690→        let status = Command::new("systemctl")
   691→            .args(["stop", "gururmm-agent"])
   692→            .status()
   693→            .context("Failed to stop service")?;
   694→
   695→        if status.success() {
   696→            println!("** Service stopped successfully");
   697→        } else {
   698→            anyhow::bail!("Failed to stop service");
   699→        }
   700→
   701→        Ok(())
   702→    }
   703→
   704→    #[cfg(target_os = "macos")]
   705→    {
   706→        todo!("macOS service stop not yet implemented");
   707→    }
   708→}
   709→
   710→/// Show agent status
   711→async fn show_status(config_path: PathBuf) -> Result<()> {
   712→    // On Windows, show service status
   713→    #[cfg(windows)]
   714→    {
   715→        service::windows::status()?;
   716→        println!();
   717→    }
   718→
   719→    // Try to load config for additional info
   720→    match AgentConfig::load(&config_path) {
   721→        Ok(config) => {
   722→            println!("Configuration");
   723→            println!("=============");
   724→            println!("Config file: {:?}", config_path);
   725→            println!("Server URL: {}", config.server.url);
   726→            println!("Metrics interval: {} seconds", config.metrics.interval_seconds);
   727→            println!("Watchdog enabled: {}", config.watchdog.enabled);
   728→
   729→            // Collect current metrics
   730→            let collector = MetricsCollector::new();
   731→            let metrics = collector.collect().await;
   732→
   733→            println!("\nCurrent System Metrics:");
   734→            println!("  CPU Usage: {:.1}%", metrics.cpu_percent);
   735→            println!("  Memory Usage: {:.1}%", metrics.memory_percent);
   736→            println!(
   737→                "  Memory Used: {:.2} GB",
   738→                metrics.memory_used_bytes as f64 / 1_073_741_824.0
   739→            );
   740→            println!("  Disk Usage: {:.1}%", metrics.disk_percent);
   741→            println!(
   742→                "  Disk Used: {:.2} GB",
   743→                metrics.disk_used_bytes as f64 / 1_073_741_824.0
   744→            );
   745→        }
   746→        Err(_) => {
   747→            println!("\nConfig file {:?} not found or invalid.", config_path);
   748→            #[cfg(windows)]
   749→            println!("Service config location: {}\\agent.toml", service::windows::CONFIG_DIR);
   750→        }
   751→    }
   752→
   753→    Ok(())
   754→}
   755→
   756→/// Generate a sample configuration file
   757→async fn generate_config(output: PathBuf) -> Result<()> {
   758→    let sample_config = AgentConfig::sample();
   759→    let toml_str = toml::to_string_pretty(&sample_config)?;
   760→
   761→    std::fs::write(&output, toml_str)?;
   762→    println!("Sample configuration written to {:?}", output);
   763→    println!("\nEdit this file with your server URL and API key, then run:");
   764→    println!("  gururmm-agent --config {:?} run", output);
   765→
   766→    Ok(())
   767→}
   768→

<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>

//! 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(())
}


Now I need to:
1. Add the `tray_launcher` module declaration
2. Integrate the tray launcher into the `run_agent` function
3. Update the Windows service code in `service.rs` to also use the tray launcher
4. Update the install command to copy the tray binary

Let me start by adding the module and integrating it into main.rs.


Now let me integrate the tray launcher into main.rs:


Edit

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

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