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