1→//! Windows Service implementation for GuruRMM Agent 2→//! 3→//! This module implements the Windows Service Control Manager (SCM) protocol, 4→//! allowing the agent to run as a native Windows service without third-party wrappers. 5→ 6→#[cfg(all(windows, feature = "native-service"))] 7→pub mod windows { 8→ use std::ffi::OsString; 9→ use std::path::PathBuf; 10→ use std::sync::mpsc; 11→ use std::time::Duration; 12→ 13→ use anyhow::{Context, Result}; 14→ use tracing::{error, info, warn}; 15→ use windows_service::{ 16→ define_windows_service, 17→ service::{ 18→ ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, 19→ ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, 20→ ServiceType, 21→ }, 22→ service_control_handler::{self, ServiceControlHandlerResult}, 23→ service_dispatcher, service_manager::{ServiceManager, ServiceManagerAccess}, 24→ }; 25→ 26→ pub const SERVICE_NAME: &str = "GuruRMMAgent"; 27→ pub const SERVICE_DISPLAY_NAME: &str = "GuruRMM Agent"; 28→ pub const SERVICE_DESCRIPTION: &str = 29→ "GuruRMM Agent - Remote Monitoring and Management service"; 30→ pub const INSTALL_DIR: &str = r"C:\Program Files\GuruRMM"; 31→ pub const CONFIG_DIR: &str = r"C:\ProgramData\GuruRMM"; 32→ 33→ // Generate the Windows service boilerplate 34→ define_windows_service!(ffi_service_main, service_main); 35→ 36→ /// Entry point called by the Windows Service Control Manager 37→ pub fn run_as_service() -> Result<()> { 38→ // This function is called when Windows starts the service. 39→ // It blocks until the service is stopped. 40→ service_dispatcher::start(SERVICE_NAME, ffi_service_main) 41→ .context("Failed to start service dispatcher")?; 42→ Ok(()) 43→ } 44→ 45→ /// Main service function called by the SCM 46→ fn service_main(arguments: Vec) { 47→ if let Err(e) = run_service(arguments) { 48→ error!("Service error: {}", e); 49→ } 50→ } 51→ 52→ /// The actual service implementation 53→ fn run_service(_arguments: Vec) -> Result<()> { 54→ // Create a channel to receive stop events 55→ let (shutdown_tx, shutdown_rx) = mpsc::channel(); 56→ 57→ // Create the service control handler 58→ let event_handler = move |control_event| -> ServiceControlHandlerResult { 59→ match control_event { 60→ ServiceControl::Stop => { 61→ info!("Received stop command from SCM"); 62→ let _ = shutdown_tx.send(()); 63→ ServiceControlHandlerResult::NoError 64→ } 65→ ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, 66→ ServiceControl::Shutdown => { 67→ info!("Received shutdown command from SCM"); 68→ let _ = shutdown_tx.send(()); 69→ ServiceControlHandlerResult::NoError 70→ } 71→ _ => ServiceControlHandlerResult::NotImplemented, 72→ } 73→ }; 74→ 75→ // Register the service control handler 76→ let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) 77→ .context("Failed to register service control handler")?; 78→ 79→ // Report that we're starting 80→ status_handle 81→ .set_service_status(ServiceStatus { 82→ service_type: ServiceType::OWN_PROCESS, 83→ current_state: ServiceState::StartPending, 84→ controls_accepted: ServiceControlAccept::empty(), 85→ exit_code: ServiceExitCode::Win32(0), 86→ checkpoint: 0, 87→ wait_hint: Duration::from_secs(10), 88→ process_id: None, 89→ }) 90→ .context("Failed to set StartPending status")?; 91→ 92→ // Determine config path 93→ let config_path = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR)); 94→ 95→ // Create the tokio runtime for the agent 96→ let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?; 97→ 98→ // Start the agent in the runtime 99→ let agent_result = runtime.block_on(async { 100→ // Load configuration 101→ let config = match crate::config::AgentConfig::load(&config_path) { 102→ Ok(c) => c, 103→ Err(e) => { 104→ error!("Failed to load config from {:?}: {}", config_path, e); 105→ return Err(anyhow::anyhow!("Config load failed: {}", e)); 106→ } 107→ }; 108→ 109→ info!("GuruRMM Agent service starting..."); 110→ info!("Config loaded from {:?}", config_path); 111→ info!("Server URL: {}", config.server.url); 112→ 113→ // Initialize metrics collector 114→ let metrics_collector = crate::metrics::MetricsCollector::new(); 115→ info!("Metrics collector initialized"); 116→ 117→ // Create shared state 118→ let state = std::sync::Arc::new(crate::AppState { 119→ config: config.clone(), 120→ metrics_collector, 121→ connected: tokio::sync::RwLock::new(false), 122→ last_checkin: tokio::sync::RwLock::new(None), 123→ tray_policy: tokio::sync::RwLock::new(crate::ipc::TrayPolicy::default_permissive()), 124→ }); 125→ 126→ // Report that we're running 127→ status_handle 128→ .set_service_status(ServiceStatus { 129→ service_type: ServiceType::OWN_PROCESS, 130→ current_state: ServiceState::Running, 131→ controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, 132→ exit_code: ServiceExitCode::Win32(0), 133→ checkpoint: 0, 134→ wait_hint: Duration::default(), 135→ process_id: None, 136→ }) 137→ .context("Failed to set Running status")?; 138→ 139→ // Start WebSocket client task 140→ let ws_state = std::sync::Arc::clone(&state); 141→ let ws_handle = tokio::spawn(async move { 142→ loop { 143→ info!("Connecting to server..."); 144→ match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone( 145→ &ws_state, 146→ )) 147→ .await 148→ { 149→ Ok(_) => { 150→ warn!("WebSocket connection closed normally, reconnecting..."); 151→ } 152→ Err(e) => { 153→ error!("WebSocket error: {}, reconnecting in 10 seconds...", e); 154→ } 155→ } 156→ *ws_state.connected.write().await = false; 157→ tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; 158→ } 159→ }); 160→ 161→ // Start metrics collection task 162→ let metrics_state = std::sync::Arc::clone(&state); 163→ let metrics_handle = tokio::spawn(async move { 164→ let interval = metrics_state.config.metrics.interval_seconds; 165→ let mut interval_timer = 166→ tokio::time::interval(tokio::time::Duration::from_secs(interval)); 167→ 168→ loop { 169→ interval_timer.tick().await; 170→ let metrics = metrics_state.metrics_collector.collect().await; 171→ if *metrics_state.connected.read().await { 172→ info!( 173→ "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%", 174→ metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent 175→ ); 176→ } 177→ } 178→ }); 179→ 180→ // Wait for shutdown signal from SCM 181→ // We use a separate task to poll the channel since it's not async 182→ let shutdown_handle = tokio::spawn(async move { 183→ loop { 184→ match shutdown_rx.try_recv() { 185→ Ok(_) => { 186→ info!("Shutdown signal received"); 187→ break; 188→ } 189→ Err(mpsc::TryRecvError::Empty) => { 190→ tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; 191→ } 192→ Err(mpsc::TryRecvError::Disconnected) => { 193→ warn!("Shutdown channel disconnected"); 194→ break; 195→ } 196→ } 197→ } 198→ }); 199→ 200→ // Wait for shutdown 201→ tokio::select! { 202→ _ = shutdown_handle => { 203→ info!("Service shutting down gracefully"); 204→ } 205→ _ = ws_handle => { 206→ error!("WebSocket task ended unexpectedly"); 207→ } 208→ _ = metrics_handle => { 209→ error!("Metrics task ended unexpectedly"); 210→ } 211→ } 212→ 213→ Ok::<(), anyhow::Error>(()) 214→ }); 215→ 216→ // Report that we're stopping 217→ status_handle 218→ .set_service_status(ServiceStatus { 219→ service_type: ServiceType::OWN_PROCESS, 220→ current_state: ServiceState::StopPending, 221→ controls_accepted: ServiceControlAccept::empty(), 222→ exit_code: ServiceExitCode::Win32(0), 223→ checkpoint: 0, 224→ wait_hint: Duration::from_secs(5), 225→ process_id: None, 226→ }) 227→ .ok(); 228→ 229→ // Report that we've stopped 230→ status_handle 231→ .set_service_status(ServiceStatus { 232→ service_type: ServiceType::OWN_PROCESS, 233→ current_state: ServiceState::Stopped, 234→ controls_accepted: ServiceControlAccept::empty(), 235→ exit_code: match &agent_result { 236→ Ok(_) => ServiceExitCode::Win32(0), 237→ Err(_) => ServiceExitCode::Win32(1), 238→ }, 239→ checkpoint: 0, 240→ wait_hint: Duration::default(), 241→ process_id: None, 242→ }) 243→ .ok(); 244→ 245→ agent_result 246→ } 247→ 248→ /// Known legacy service names to check and remove 249→ const LEGACY_SERVICE_NAMES: &[&str] = &[ 250→ "GuruRMM-Agent", // NSSM-based service name 251→ "gururmm-agent", // Alternative casing 252→ ]; 253→ 254→ /// Detect and remove legacy service installations (e.g., NSSM-based) 255→ fn cleanup_legacy_services() -> Result<()> { 256→ let manager = match ServiceManager::local_computer( 257→ None::<&str>, 258→ ServiceManagerAccess::CONNECT, 259→ ) { 260→ Ok(m) => m, 261→ Err(_) => return Ok(()), // Can't connect, skip legacy cleanup 262→ }; 263→ 264→ for legacy_name in LEGACY_SERVICE_NAMES { 265→ if let Ok(service) = manager.open_service( 266→ *legacy_name, 267→ ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, 268→ ) { 269→ info!("Found legacy service '{}', removing...", legacy_name); 270→ 271→ // Stop if running 272→ if let Ok(status) = service.query_status() { 273→ if status.current_state != ServiceState::Stopped { 274→ info!("Stopping legacy service..."); 275→ let _ = service.stop(); 276→ std::thread::sleep(Duration::from_secs(3)); 277→ } 278→ } 279→ 280→ // Delete the service 281→ match service.delete() { 282→ Ok(_) => { 283→ println!("** Removed legacy service: {}", legacy_name); 284→ } 285→ Err(e) => { 286→ warn!("Failed to delete legacy service '{}': {}", legacy_name, e); 287→ } 288→ } 289→ } 290→ } 291→ 292→ // Also check for NSSM in registry/service config 293→ // NSSM services have specific registry keys under HKLM\SYSTEM\CurrentControlSet\Services\{name}\Parameters 294→ for legacy_name in LEGACY_SERVICE_NAMES { 295→ let params_key = format!( 296→ r"SYSTEM\CurrentControlSet\Services\{}\Parameters", 297→ legacy_name 298→ ); 299→ // If this key exists, it was likely an NSSM service 300→ if let Ok(output) = std::process::Command::new("reg") 301→ .args(["query", &format!(r"HKLM\{}", params_key)]) 302→ .output() 303→ { 304→ if output.status.success() { 305→ info!("Found NSSM registry keys for '{}', cleaning up...", legacy_name); 306→ let _ = std::process::Command::new("reg") 307→ .args(["delete", &format!(r"HKLM\{}", params_key), "/f"]) 308→ .output(); 309→ } 310→ } 311→ } 312→ 313→ Ok(()) 314→ } 315→ 316→ /// Install the agent as a Windows service using native APIs 317→ pub fn install( 318→ server_url: Option, 319→ api_key: Option, 320→ skip_legacy_check: bool, 321→ ) -> Result<()> { 322→ info!("Installing GuruRMM Agent as Windows service..."); 323→ 324→ // Clean up legacy installations unless skipped 325→ if !skip_legacy_check { 326→ info!("Checking for legacy service installations..."); 327→ if let Err(e) = cleanup_legacy_services() { 328→ warn!("Legacy cleanup warning: {}", e); 329→ } 330→ } 331→ 332→ // Get the current executable path 333→ let current_exe = 334→ std::env::current_exe().context("Failed to get current executable path")?; 335→ 336→ let binary_dest = PathBuf::from(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR)); 337→ let config_dest = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR)); 338→ 339→ // Create directories 340→ info!("Creating directories..."); 341→ std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?; 342→ std::fs::create_dir_all(CONFIG_DIR).context("Failed to create config directory")?; 343→ 344→ // Copy binary 345→ info!("Copying binary to: {:?}", binary_dest); 346→ std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?; 347→ 348→ // Handle configuration 349→ let config_needs_manual_edit; 350→ if !config_dest.exists() { 351→ info!("Creating config: {:?}", config_dest); 352→ 353→ // Start with sample config 354→ let mut config = crate::config::AgentConfig::sample(); 355→ 356→ // Apply provided values 357→ if let Some(url) = &server_url { 358→ config.server.url = url.clone(); 359→ } 360→ if let Some(key) = &api_key { 361→ config.server.api_key = key.clone(); 362→ } 363→ 364→ let toml_str = toml::to_string_pretty(&config)?; 365→ std::fs::write(&config_dest, toml_str).context("Failed to write config file")?; 366→ 367→ config_needs_manual_edit = server_url.is_none() || api_key.is_none(); 368→ } else { 369→ info!("Config already exists: {:?}", config_dest); 370→ config_needs_manual_edit = false; 371→ 372→ // If server_url or api_key provided, update existing config 373→ if server_url.is_some() || api_key.is_some() { 374→ info!("Updating existing configuration..."); 375→ let config_content = std::fs::read_to_string(&config_dest)?; 376→ let mut config: crate::config::AgentConfig = toml::from_str(&config_content) 377→ .context("Failed to parse existing config")?; 378→ 379→ if let Some(url) = &server_url { 380→ config.server.url = url.clone(); 381→ } 382→ if let Some(key) = &api_key { 383→ config.server.api_key = key.clone(); 384→ } 385→ 386→ let toml_str = toml::to_string_pretty(&config)?; 387→ std::fs::write(&config_dest, toml_str) 388→ .context("Failed to update config file")?; 389→ } 390→ } 391→ 392→ // Open the service manager 393→ let manager = ServiceManager::local_computer( 394→ None::<&str>, 395→ ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, 396→ ) 397→ .context("Failed to connect to Service Control Manager. Run as Administrator.")?; 398→ 399→ // Check if service already exists 400→ if let Ok(service) = manager.open_service( 401→ SERVICE_NAME, 402→ ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP, 403→ ) { 404→ info!("Removing existing service..."); 405→ 406→ // Stop the service if running 407→ if let Ok(status) = service.query_status() { 408→ if status.current_state != ServiceState::Stopped { 409→ let _ = service.stop(); 410→ std::thread::sleep(Duration::from_secs(2)); 411→ } 412→ } 413→ 414→ // Delete the service 415→ service.delete().context("Failed to delete existing service")?; 416→ drop(service); 417→ 418→ // Wait for deletion to complete 419→ std::thread::sleep(Duration::from_secs(2)); 420→ } 421→ 422→ // Create the service 423→ // The service binary is called with "service" subcommand when started by SCM 424→ let service_binary_path = format!(r#""{}" service"#, binary_dest.display()); 425→ 426→ info!("Creating service with path: {}", service_binary_path); 427→ 428→ let service_info = ServiceInfo { 429→ name: OsString::from(SERVICE_NAME), 430→ display_name: OsString::from(SERVICE_DISPLAY_NAME), 431→ service_type: ServiceType::OWN_PROCESS, 432→ start_type: ServiceStartType::AutoStart, 433→ error_control: ServiceErrorControl::Normal, 434→ executable_path: binary_dest.clone(), 435→ launch_arguments: vec![OsString::from("service")], 436→ dependencies: vec![], 437→ account_name: None, // LocalSystem 438→ account_password: None, 439→ }; 440→ 441→ let service = manager 442→ .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START) 443→ .context("Failed to create service")?; 444→ 445→ // Set description 446→ service 447→ .set_description(SERVICE_DESCRIPTION) 448→ .context("Failed to set service description")?; 449→ 450→ // Configure recovery options using sc.exe (windows-service crate doesn't support this directly) 451→ info!("Configuring recovery options..."); 452→ let _ = std::process::Command::new("sc") 453→ .args([ 454→ "failure", 455→ SERVICE_NAME, 456→ "reset=86400", 457→ "actions=restart/60000/restart/60000/restart/60000", 458→ ]) 459→ .output(); 460→ 461→ println!("\n** GuruRMM Agent installed successfully!"); 462→ println!("\nInstalled files:"); 463→ println!(" Binary: {:?}", binary_dest); 464→ println!(" Config: {:?}", config_dest); 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:"); 471→ println!(" gururmm-agent start"); 472→ println!(" Or: sc start {}", SERVICE_NAME); 473→ } else { 474→ println!("\nStarting service..."); 475→ if let Err(e) = start() { 476→ println!("** Failed to start service: {}. Start manually with:", e); 477→ println!(" gururmm-agent start"); 478→ } else { 479→ println!("** Service started successfully!"); 480→ } 481→ } 482→ 483→ println!("\nUseful commands:"); 484→ println!(" Status: gururmm-agent status"); 485→ println!(" Stop: gururmm-agent stop"); 486→ println!(" Start: gururmm-agent start"); 487→ 488→ Ok(()) 489→ } 490→ 491→ /// Uninstall the Windows service 492→ pub fn uninstall() -> Result<()> { 493→ info!("Uninstalling GuruRMM Agent..."); 494→ 495→ let binary_path = PathBuf::from(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR)); 496→ 497→ // Open the service manager 498→ let manager = ServiceManager::local_computer( 499→ None::<&str>, 500→ ServiceManagerAccess::CONNECT, 501→ ) 502→ .context("Failed to connect to Service Control Manager. Run as Administrator.")?; 503→ 504→ // Open the service 505→ match manager.open_service( 506→ SERVICE_NAME, 507→ ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, 508→ ) { 509→ Ok(service) => { 510→ // Stop if running 511→ if let Ok(status) = service.query_status() { 512→ if status.current_state != ServiceState::Stopped { 513→ info!("Stopping service..."); 514→ let _ = service.stop(); 515→ std::thread::sleep(Duration::from_secs(3)); 516→ } 517→ } 518→ 519→ // Delete the service 520→ info!("Deleting service..."); 521→ service.delete().context("Failed to delete service")?; 522→ } 523→ Err(_) => { 524→ warn!("Service was not installed"); 525→ } 526→ } 527→ 528→ // Remove binary 529→ if binary_path.exists() { 530→ info!("Removing binary: {:?}", binary_path); 531→ // Wait a bit for service to fully stop 532→ std::thread::sleep(Duration::from_secs(1)); 533→ if let Err(e) = std::fs::remove_file(&binary_path) { 534→ warn!("Failed to remove binary (may be in use): {}", e); 535→ } 536→ } 537→ 538→ // Remove install directory if empty 539→ let _ = std::fs::remove_dir(INSTALL_DIR); 540→ 541→ println!("\n** GuruRMM Agent uninstalled successfully!"); 542→ println!( 543→ "\nNote: Config directory {:?} was preserved.", 544→ CONFIG_DIR 545→ ); 546→ println!("Remove it manually if no longer needed."); 547→ 548→ Ok(()) 549→ } 550→ 551→ /// Start the installed service 552→ pub fn start() -> Result<()> { 553→ info!("Starting GuruRMM Agent service..."); 554→ 555→ let manager = ServiceManager::local_computer( 556→ None::<&str>, 557→ ServiceManagerAccess::CONNECT, 558→ ) 559→ .context("Failed to connect to Service Control Manager")?; 560→ 561→ let service = manager 562→ .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS) 563→ .context("Failed to open service. Is it installed?")?; 564→ 565→ service 566→ .start::(&[]) 567→ .context("Failed to start service")?; 568→ 569→ // Wait briefly and check status 570→ std::thread::sleep(Duration::from_secs(2)); 571→ 572→ let status = service.query_status()?; 573→ match status.current_state { 574→ ServiceState::Running => { 575→ println!("** Service started successfully"); 576→ println!("Check status: gururmm-agent status"); 577→ } 578→ ServiceState::StartPending => { 579→ println!("** Service is starting..."); 580→ println!("Check status: gururmm-agent status"); 581→ } 582→ other => { 583→ println!("Service state: {:?}", other); 584→ } 585→ } 586→ 587→ Ok(()) 588→ } 589→ 590→ /// Stop the installed service 591→ pub fn stop() -> Result<()> { 592→ info!("Stopping GuruRMM Agent service..."); 593→ 594→ let manager = ServiceManager::local_computer( 595→ None::<&str>, 596→ ServiceManagerAccess::CONNECT, 597→ ) 598→ .context("Failed to connect to Service Control Manager")?; 599→ 600→ let service = manager 601→ .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS) 602→ .context("Failed to open service. Is it installed?")?; 603→ 604→ service.stop().context("Failed to stop service")?; 605→ 606→ // Wait and verify 607→ std::thread::sleep(Duration::from_secs(2)); 608→ 609→ let status = service.query_status()?; 610→ match status.current_state { 611→ ServiceState::Stopped => { 612→ println!("** Service stopped successfully"); 613→ } 614→ ServiceState::StopPending => { 615→ println!("** Service is stopping..."); 616→ } 617→ other => { 618→ println!("Service state: {:?}", other); 619→ } 620→ } 621→ 622→ Ok(()) 623→ } 624→ 625→ /// Query service status 626→ pub fn status() -> Result<()> { 627→ let manager = ServiceManager::local_computer( 628→ None::<&str>, 629→ ServiceManagerAccess::CONNECT, 630→ ) 631→ .context("Failed to connect to Service Control Manager")?; 632→ 633→ match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) { 634→ Ok(service) => { 635→ let status = service.query_status()?; 636→ println!("GuruRMM Agent Service Status"); 637→ println!("============================"); 638→ println!("Service Name: {}", SERVICE_NAME); 639→ println!("Display Name: {}", SERVICE_DISPLAY_NAME); 640→ println!("State: {:?}", status.current_state); 641→ println!( 642→ "Binary: {}\\gururmm-agent.exe", 643→ INSTALL_DIR 644→ ); 645→ println!("Config: {}\\agent.toml", CONFIG_DIR); 646→ } 647→ Err(_) => { 648→ println!("GuruRMM Agent Service Status"); 649→ println!("============================"); 650→ println!("Status: NOT INSTALLED"); 651→ println!("\nTo install: gururmm-agent install"); 652→ } 653→ } 654→ 655→ Ok(()) 656→ } 657→} 658→ 659→/// Legacy Windows stub module (when native-service is not enabled) 660→/// For legacy Windows (7, Server 2008 R2), use NSSM for service wrapper 661→#[cfg(all(windows, not(feature = "native-service")))] 662→pub mod windows { 663→ use anyhow::{Result, bail}; 664→ 665→ pub const SERVICE_NAME: &str = "GuruRMMAgent"; 666→ pub const SERVICE_DISPLAY_NAME: &str = "GuruRMM Agent"; 667→ pub const SERVICE_DESCRIPTION: &str = 668→ "GuruRMM Agent - Remote Monitoring and Management service"; 669→ pub const INSTALL_DIR: &str = r"C:\Program Files\GuruRMM"; 670→ pub const CONFIG_DIR: &str = r"C:\ProgramData\GuruRMM"; 671→ 672→ /// Legacy build doesn't support native service mode 673→ pub fn run_as_service() -> Result<()> { 674→ bail!("Native Windows service mode not available in legacy build. Use 'run' command with NSSM wrapper instead.") 675→ } 676→ 677→ /// Legacy install just copies binary and config, prints NSSM instructions 678→ pub fn install( 679→ server_url: Option, 680→ api_key: Option, 681→ _skip_legacy_check: bool, 682→ ) -> Result<()> { 683→ use std::path::PathBuf; 684→ use tracing::info; 685→ 686→ info!("Installing GuruRMM Agent (legacy mode)..."); 687→ 688→ // Get the current executable path 689→ let current_exe = std::env::current_exe()?; 690→ let binary_dest = PathBuf::from(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR)); 691→ let config_dest = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR)); 692→ 693→ // Create directories 694→ std::fs::create_dir_all(INSTALL_DIR)?; 695→ std::fs::create_dir_all(CONFIG_DIR)?; 696→ 697→ // Copy binary 698→ info!("Copying binary to: {:?}", binary_dest); 699→ std::fs::copy(¤t_exe, &binary_dest)?; 700→ 701→ // Create config if needed 702→ if !config_dest.exists() { 703→ let mut config = crate::config::AgentConfig::sample(); 704→ if let Some(url) = &server_url { 705→ config.server.url = url.clone(); 706→ } 707→ if let Some(key) = &api_key { 708→ config.server.api_key = key.clone(); 709→ } 710→ let toml_str = toml::to_string_pretty(&config)?; 711→ std::fs::write(&config_dest, toml_str)?; 712→ } 713→ 714→ println!("\n** GuruRMM Agent installed (legacy mode)!"); 715→ println!("\nInstalled files:"); 716→ println!(" Binary: {:?}", binary_dest); 717→ println!(" Config: {:?}", config_dest); 718→ println!("\n** IMPORTANT: This is a legacy build for Windows 7/Server 2008 R2"); 719→ println!(" Use NSSM to install as a service:"); 720→ println!(); 721→ println!(" nssm install {} {:?} run --config {:?}", SERVICE_NAME, binary_dest, config_dest); 722→ println!(" nssm start {}", SERVICE_NAME); 723→ println!(); 724→ println!(" Download NSSM from: https://nssm.cc/download"); 725→ 726→ Ok(()) 727→ } 728→ 729→ pub fn uninstall() -> Result<()> { 730→ use std::path::PathBuf; 731→ 732→ let binary_path = PathBuf::from(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR)); 733→ 734→ println!("** To uninstall legacy service, use NSSM:"); 735→ println!(" nssm stop {}", SERVICE_NAME); 736→ println!(" nssm remove {} confirm", SERVICE_NAME); 737→ println!(); 738→ 739→ if binary_path.exists() { 740→ std::fs::remove_file(&binary_path)?; 741→ println!("** Binary removed: {:?}", binary_path); 742→ } 743→ 744→ let _ = std::fs::remove_dir(INSTALL_DIR); 745→ println!("\n** GuruRMM Agent uninstalled (legacy mode)!"); 746→ println!("Note: Config directory {} was preserved.", CONFIG_DIR); 747→ 748→ Ok(()) 749→ } 750→ 751→ pub fn start() -> Result<()> { 752→ println!("** Legacy build: Use NSSM or sc.exe to start the service:"); 753→ println!(" nssm start {}", SERVICE_NAME); 754→ println!(" -- OR --"); 755→ println!(" sc start {}", SERVICE_NAME); 756→ Ok(()) 757→ } 758→ 759→ pub fn stop() -> Result<()> { 760→ println!("** Legacy build: Use NSSM or sc.exe to stop the service:"); 761→ println!(" nssm stop {}", SERVICE_NAME); 762→ println!(" -- OR --"); 763→ println!(" sc stop {}", SERVICE_NAME); 764→ Ok(()) 765→ } 766→ 767→ pub fn status() -> Result<()> { 768→ println!("GuruRMM Agent Service Status (Legacy Build)"); 769→ println!("=========================================="); 770→ println!("Service Name: {}", SERVICE_NAME); 771→ println!(); 772→ println!("** Legacy build: Use sc.exe to query status:"); 773→ println!(" sc query {}", SERVICE_NAME); 774→ println!(); 775→ println!("Binary: {}\\gururmm-agent.exe", INSTALL_DIR); 776→ println!("Config: {}\\agent.toml", CONFIG_DIR); 777→ Ok(()) 778→ } 779→} 780→ 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.