//! Windows Service implementation for GuruRMM Agent //! //! This module implements the Windows Service Control Manager (SCM) protocol, //! allowing the agent to run as a native Windows service without third-party wrappers. #[cfg(all(windows, feature = "native-service"))] pub mod windows { use std::ffi::OsString; use std::path::PathBuf; use std::sync::mpsc; use std::time::Duration; use anyhow::{Context, Result}; use tracing::{error, info, warn}; use windows_service::{ define_windows_service, service::{ ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, }, service_control_handler::{self, ServiceControlHandlerResult}, service_dispatcher, service_manager::{ServiceManager, ServiceManagerAccess}, }; pub const SERVICE_NAME: &str = "GuruRMMAgent"; pub const SERVICE_DISPLAY_NAME: &str = "GuruRMM Agent"; pub const SERVICE_DESCRIPTION: &str = "GuruRMM Agent - Remote Monitoring and Management service"; pub const INSTALL_DIR: &str = r"C:\Program Files\GuruRMM"; pub const CONFIG_DIR: &str = r"C:\ProgramData\GuruRMM"; // Generate the Windows service boilerplate define_windows_service!(ffi_service_main, service_main); /// Entry point called by the Windows Service Control Manager pub fn run_as_service() -> Result<()> { // This function is called when Windows starts the service. // It blocks until the service is stopped. service_dispatcher::start(SERVICE_NAME, ffi_service_main) .context("Failed to start service dispatcher")?; Ok(()) } /// Main service function called by the SCM fn service_main(arguments: Vec) { if let Err(e) = run_service(arguments) { error!("Service error: {}", e); } } /// The actual service implementation fn run_service(_arguments: Vec) -> Result<()> { // Create a channel to receive stop events let (shutdown_tx, shutdown_rx) = mpsc::channel(); // Create the service control handler let event_handler = move |control_event| -> ServiceControlHandlerResult { match control_event { ServiceControl::Stop => { info!("Received stop command from SCM"); let _ = shutdown_tx.send(()); ServiceControlHandlerResult::NoError } ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, ServiceControl::Shutdown => { info!("Received shutdown command from SCM"); let _ = shutdown_tx.send(()); ServiceControlHandlerResult::NoError } _ => ServiceControlHandlerResult::NotImplemented, } }; // Register the service control handler let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) .context("Failed to register service control handler")?; // Report that we're starting status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::StartPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::from_secs(10), process_id: None, }) .context("Failed to set StartPending status")?; // Determine config path let config_path = PathBuf::from(CONFIG_DIR).join("agent.toml"); // Create the tokio runtime for the agent let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?; // Start the agent in the runtime let agent_result = runtime.block_on(async { // Load configuration let config = match crate::config::AgentConfig::load(&config_path) { Ok(c) => c, Err(e) => { error!("Failed to load config from {:?}: {}", config_path, e); return Err(anyhow::anyhow!("Config load failed: {}", e)); } }; info!("GuruRMM Agent service starting..."); info!("Config loaded from {:?}", config_path); info!("Server URL: {}", config.server.url); // Initialize metrics collector let metrics_collector = crate::metrics::MetricsCollector::new(); info!("Metrics collector initialized"); // Create shared state let state = std::sync::Arc::new(crate::AppState { config: config.clone(), metrics_collector, connected: tokio::sync::RwLock::new(false), }); // Report that we're running status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .context("Failed to set Running status")?; // Start WebSocket client task let ws_state = std::sync::Arc::clone(&state); let ws_handle = tokio::spawn(async move { loop { info!("Connecting to server..."); match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone( &ws_state, )) .await { Ok(_) => { warn!("WebSocket connection closed normally, reconnecting..."); } Err(e) => { error!("WebSocket error: {}, reconnecting in 10 seconds...", e); } } *ws_state.connected.write().await = false; tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; } }); // Start metrics collection task let metrics_state = std::sync::Arc::clone(&state); let metrics_handle = tokio::spawn(async move { let interval = metrics_state.config.metrics.interval_seconds; let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval)); loop { interval_timer.tick().await; let metrics = metrics_state.metrics_collector.collect().await; if *metrics_state.connected.read().await { info!( "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%", metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent ); } } }); // Wait for shutdown signal from SCM // We use a separate task to poll the channel since it's not async let shutdown_handle = tokio::spawn(async move { loop { match shutdown_rx.try_recv() { Ok(_) => { info!("Shutdown signal received"); break; } Err(mpsc::TryRecvError::Empty) => { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } Err(mpsc::TryRecvError::Disconnected) => { warn!("Shutdown channel disconnected"); break; } } } }); // Wait for shutdown tokio::select! { _ = shutdown_handle => { info!("Service shutting down gracefully"); } _ = ws_handle => { error!("WebSocket task ended unexpectedly"); } _ = metrics_handle => { error!("Metrics task ended unexpectedly"); } } Ok::<(), anyhow::Error>(()) }); // Report that we're stopping status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::StopPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::from_secs(5), process_id: None, }) .ok(); // Report that we've stopped status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code: match &agent_result { Ok(_) => ServiceExitCode::Win32(0), Err(_) => ServiceExitCode::Win32(1), }, checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .ok(); agent_result } /// Known legacy service names to check and remove const LEGACY_SERVICE_NAMES: &[&str] = &[ "GuruRMM-Agent", // NSSM-based service name "gururmm-agent", // Alternative casing ]; /// Detect and remove legacy service installations (e.g., NSSM-based) fn cleanup_legacy_services() -> Result<()> { let manager = match ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) { Ok(m) => m, Err(_) => return Ok(()), // Can't connect, skip legacy cleanup }; for legacy_name in LEGACY_SERVICE_NAMES { if let Ok(service) = manager.open_service( *legacy_name, ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, ) { info!("Found legacy service '{}', removing...", legacy_name); // Stop if running if let Ok(status) = service.query_status() { if status.current_state != ServiceState::Stopped { info!("Stopping legacy service..."); let _ = service.stop(); std::thread::sleep(Duration::from_secs(3)); } } // Delete the service match service.delete() { Ok(_) => { println!("** Removed legacy service: {}", legacy_name); } Err(e) => { warn!("Failed to delete legacy service '{}': {}", legacy_name, e); } } } } // Also check for NSSM in registry/service config // NSSM services have specific registry keys under HKLM\SYSTEM\CurrentControlSet\Services\{name}\Parameters for legacy_name in LEGACY_SERVICE_NAMES { let params_key = format!( r"SYSTEM\CurrentControlSet\Services\{}\Parameters", legacy_name ); // If this key exists, it was likely an NSSM service if let Ok(output) = std::process::Command::new("reg") .args(["query", &format!(r"HKLM\{}", params_key)]) .output() { if output.status.success() { info!("Found NSSM registry keys for '{}', cleaning up...", legacy_name); let _ = std::process::Command::new("reg") .args(["delete", &format!(r"HKLM\{}", params_key), "/f"]) .output(); } } } Ok(()) } /// Install the agent as a Windows service using native APIs pub fn install( server_url: Option, api_key: Option, skip_legacy_check: bool, ) -> Result<()> { info!("Installing GuruRMM Agent as Windows service..."); // Clean up legacy installations unless skipped if !skip_legacy_check { info!("Checking for legacy service installations..."); if let Err(e) = cleanup_legacy_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 = PathBuf::from(INSTALL_DIR).join("gururmm-agent.exe"); let config_dest = PathBuf::from(CONFIG_DIR).join("agent.toml"); // Create directories info!("Creating directories..."); std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?; std::fs::create_dir_all(CONFIG_DIR).context("Failed to create config directory")?; // Copy binary info!("Copying binary to: {:?}", binary_dest); std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?; // Handle configuration let config_needs_manual_edit; if !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")?; 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")?; } } // Open the service manager let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, ) .context("Failed to connect to Service Control Manager. Run as Administrator.")?; // Check if service already exists if let Ok(service) = manager.open_service( SERVICE_NAME, ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP, ) { info!("Removing existing service..."); // Stop the service if running if let Ok(status) = service.query_status() { if status.current_state != ServiceState::Stopped { let _ = service.stop(); std::thread::sleep(Duration::from_secs(2)); } } // Delete the service service.delete().context("Failed to delete existing service")?; drop(service); // Wait for deletion to complete std::thread::sleep(Duration::from_secs(2)); } // Create the service // The service binary is called with "service" subcommand when started by SCM let service_binary_path = format!(r#""{}" service"#, binary_dest.display()); info!("Creating service with path: {}", service_binary_path); let service_info = ServiceInfo { name: OsString::from(SERVICE_NAME), display_name: OsString::from(SERVICE_DISPLAY_NAME), service_type: ServiceType::OWN_PROCESS, start_type: ServiceStartType::AutoStart, error_control: ServiceErrorControl::Normal, executable_path: binary_dest.clone(), launch_arguments: vec![OsString::from("service")], dependencies: vec![], account_name: None, // LocalSystem account_password: None, }; let service = manager .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START) .context("Failed to create service")?; // Set description service .set_description(SERVICE_DESCRIPTION) .context("Failed to set service description")?; // Configure recovery options using sc.exe (windows-service crate doesn't support this directly) info!("Configuring recovery options..."); let _ = std::process::Command::new("sc") .args([ "failure", SERVICE_NAME, "reset=86400", "actions=restart/60000/restart/60000/restart/60000", ]) .output(); println!("\n** GuruRMM Agent installed successfully!"); println!("\nInstalled files:"); println!(" Binary: {:?}", binary_dest); println!(" Config: {:?}", config_dest); 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:"); println!(" gururmm-agent start"); println!(" Or: sc start {}", SERVICE_NAME); } else { println!("\nStarting service..."); if let Err(e) = start() { println!("** Failed to start service: {}. Start manually with:", e); println!(" gururmm-agent start"); } else { println!("** Service started successfully!"); } } println!("\nUseful commands:"); println!(" Status: gururmm-agent status"); println!(" Stop: gururmm-agent stop"); println!(" Start: gururmm-agent start"); Ok(()) } /// Uninstall the Windows service pub fn uninstall() -> Result<()> { info!("Uninstalling GuruRMM Agent..."); let binary_path = PathBuf::from(INSTALL_DIR).join("gururmm-agent.exe"); // Open the service manager let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager. Run as Administrator.")?; // Open the service match manager.open_service( SERVICE_NAME, ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, ) { Ok(service) => { // Stop if running if let Ok(status) = service.query_status() { if status.current_state != ServiceState::Stopped { info!("Stopping service..."); let _ = service.stop(); std::thread::sleep(Duration::from_secs(3)); } } // Delete the service info!("Deleting service..."); service.delete().context("Failed to delete service")?; } Err(_) => { warn!("Service was not installed"); } } // Remove binary if binary_path.exists() { info!("Removing binary: {:?}", binary_path); // Wait a bit for service to fully stop std::thread::sleep(Duration::from_secs(1)); if let Err(e) = std::fs::remove_file(&binary_path) { warn!("Failed to remove binary (may be in use): {}", e); } } // Remove install directory if empty let _ = std::fs::remove_dir(INSTALL_DIR); println!("\n** GuruRMM Agent uninstalled successfully!"); println!( "\nNote: Config directory {:?} was preserved.", CONFIG_DIR ); println!("Remove it manually if no longer needed."); Ok(()) } /// Start the installed service pub fn start() -> Result<()> { info!("Starting GuruRMM Agent service..."); let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager")?; let service = manager .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS) .context("Failed to open service. Is it installed?")?; service .start::(&[]) .context("Failed to start service")?; // Wait briefly and check status std::thread::sleep(Duration::from_secs(2)); let status = service.query_status()?; match status.current_state { ServiceState::Running => { println!("** Service started successfully"); println!("Check status: gururmm-agent status"); } ServiceState::StartPending => { println!("** Service is starting..."); println!("Check status: gururmm-agent status"); } other => { println!("Service state: {:?}", other); } } Ok(()) } /// Stop the installed service pub fn stop() -> Result<()> { info!("Stopping GuruRMM Agent service..."); let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager")?; let service = manager .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS) .context("Failed to open service. Is it installed?")?; service.stop().context("Failed to stop service")?; // Wait and verify std::thread::sleep(Duration::from_secs(2)); let status = service.query_status()?; match status.current_state { ServiceState::Stopped => { println!("** Service stopped successfully"); } ServiceState::StopPending => { println!("** Service is stopping..."); } other => { println!("Service state: {:?}", other); } } Ok(()) } /// Query service status pub fn status() -> Result<()> { let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager")?; match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) { Ok(service) => { let status = service.query_status()?; println!("GuruRMM Agent Service Status"); println!("============================"); println!("Service Name: {}", SERVICE_NAME); println!("Display Name: {}", SERVICE_DISPLAY_NAME); println!("State: {:?}", status.current_state); println!( "Binary: {}\\gururmm-agent.exe", INSTALL_DIR ); println!("Config: {}\\agent.toml", CONFIG_DIR); } Err(_) => { println!("GuruRMM Agent Service Status"); println!("============================"); println!("Status: NOT INSTALLED"); println!("\nTo install: gururmm-agent install"); } } Ok(()) } } /// Legacy Windows stub module (when native-service is not enabled) /// For legacy Windows (7, Server 2008 R2), use NSSM for service wrapper #[cfg(all(windows, not(feature = "native-service")))] pub mod windows { use anyhow::{Result, bail}; pub const SERVICE_NAME: &str = "GuruRMMAgent"; pub const SERVICE_DISPLAY_NAME: &str = "GuruRMM Agent"; pub const SERVICE_DESCRIPTION: &str = "GuruRMM Agent - Remote Monitoring and Management service"; pub const INSTALL_DIR: &str = r"C:\Program Files\GuruRMM"; pub const CONFIG_DIR: &str = r"C:\ProgramData\GuruRMM"; /// Legacy build doesn't support native service mode pub fn run_as_service() -> Result<()> { bail!("Native Windows service mode not available in legacy build. Use 'run' command with NSSM wrapper instead.") } /// Legacy install just copies binary and config, prints NSSM instructions pub fn install( server_url: Option, api_key: Option, _skip_legacy_check: bool, ) -> Result<()> { use std::path::PathBuf; use tracing::info; info!("Installing GuruRMM Agent (legacy mode)..."); // Get the current executable path let current_exe = std::env::current_exe()?; let binary_dest = PathBuf::from(INSTALL_DIR).join("gururmm-agent.exe"); let config_dest = PathBuf::from(CONFIG_DIR).join("agent.toml"); // Create directories std::fs::create_dir_all(INSTALL_DIR)?; std::fs::create_dir_all(CONFIG_DIR)?; // Copy binary info!("Copying binary to: {:?}", binary_dest); std::fs::copy(¤t_exe, &binary_dest)?; // Create config if needed if !config_dest.exists() { let mut config = crate::config::AgentConfig::sample(); 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)?; } println!("\n** GuruRMM Agent installed (legacy mode)!"); println!("\nInstalled files:"); println!(" Binary: {:?}", binary_dest); println!(" Config: {:?}", config_dest); println!("\n** IMPORTANT: This is a legacy build for Windows 7/Server 2008 R2"); println!(" Use NSSM to install as a service:"); println!(); println!(" nssm install {} {:?} run --config {:?}", SERVICE_NAME, binary_dest, config_dest); println!(" nssm start {}", SERVICE_NAME); println!(); println!(" Download NSSM from: https://nssm.cc/download"); Ok(()) } pub fn uninstall() -> Result<()> { use std::path::PathBuf; let binary_path = PathBuf::from(INSTALL_DIR).join("gururmm-agent.exe"); println!("** To uninstall legacy service, use NSSM:"); println!(" nssm stop {}", SERVICE_NAME); println!(" nssm remove {} confirm", SERVICE_NAME); println!(); if binary_path.exists() { std::fs::remove_file(&binary_path)?; println!("** Binary removed: {:?}", binary_path); } let _ = std::fs::remove_dir(INSTALL_DIR); println!("\n** GuruRMM Agent uninstalled (legacy mode)!"); println!("Note: Config directory {} was preserved.", CONFIG_DIR); Ok(()) } pub fn start() -> Result<()> { println!("** Legacy build: Use NSSM or sc.exe to start the service:"); println!(" nssm start {}", SERVICE_NAME); println!(" -- OR --"); println!(" sc start {}", SERVICE_NAME); Ok(()) } pub fn stop() -> Result<()> { println!("** Legacy build: Use NSSM or sc.exe to stop the service:"); println!(" nssm stop {}", SERVICE_NAME); println!(" -- OR --"); println!(" sc stop {}", SERVICE_NAME); Ok(()) } pub fn status() -> Result<()> { println!("GuruRMM Agent Service Status (Legacy Build)"); println!("=========================================="); println!("Service Name: {}", SERVICE_NAME); println!(); println!("** Legacy build: Use sc.exe to query status:"); println!(" sc query {}", SERVICE_NAME); println!(); println!("Binary: {}\\gururmm-agent.exe", INSTALL_DIR); println!("Config: {}\\agent.toml", CONFIG_DIR); Ok(()) } }