Database: - Add missing indexes for api_key_hash, status, metrics queries - New migration: 005_add_missing_indexes.sql Server: - Fix WebSocket Ping/Pong protocol (RFC 6455 compliance) - Use separate channel for Pong responses Agent: - Replace format!() path construction with PathBuf::join() - Replace todo!() macros with proper errors for macOS support Dashboard: - Fix duplicate filter values in Agents page (__unassigned__ sentinel) - Add onError handlers to all mutations in Agents, Clients, Sites pages All changes reviewed and approved. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
778 lines
29 KiB
Rust
778 lines
29 KiB
Rust
//! 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<OsString>) {
|
|
if let Err(e) = run_service(arguments) {
|
|
error!("Service error: {}", e);
|
|
}
|
|
}
|
|
|
|
/// The actual service implementation
|
|
fn run_service(_arguments: Vec<OsString>) -> 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<String>,
|
|
api_key: Option<String>,
|
|
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::<String>(&[])
|
|
.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<String>,
|
|
api_key: Option<String>,
|
|
_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(())
|
|
}
|
|
}
|