Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection and enhanced agent documentation framework. VPN Configuration (PST-NW-VPN): - Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS - Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24) - Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment - Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2 - Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic - Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes) - Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper - vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts - OpenVPN config files (Windows-compatible, abandoned for L2TP) Key VPN Implementation Details: - L2TP creates PPP adapter with connection name as interface description - UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24 - Split-tunnel enabled (only remote traffic through VPN) - All-user connection for pre-login auto-connect via scheduled task - Authentication: CHAP + MSChapv2 for UniFi compatibility Agent Documentation: - AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents - documentation-squire.md: Documentation and task management specialist agent - Updated all agent markdown files with standardized formatting Project Organization: - Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs) - Cleaned up old session JSONL files from projects/msp-tools/ - Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows) - Added guru-rmm server components and deployment configs Technical Notes: - VPN IP pool: 192.168.4.x (client gets 192.168.4.6) - Remote network: 192.168.0.0/24 (router at 192.168.0.10) - PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7 - Credentials: pst-admin / 24Hearts$ Files: 15 VPN scripts, 2 agent docs, conversation log reorganization, guru-connect/guru-rmm infrastructure additions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
777
projects/msp-tools/guru-rmm/agent/src/service.rs
Normal file
777
projects/msp-tools/guru-rmm/agent/src/service.rs
Normal file
@@ -0,0 +1,777 @@
|
||||
//! 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(format!(r"{}\\agent.toml", CONFIG_DIR));
|
||||
|
||||
// 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(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR));
|
||||
let config_dest = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR));
|
||||
|
||||
// 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(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR));
|
||||
|
||||
// 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(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR));
|
||||
let config_dest = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR));
|
||||
|
||||
// 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(format!(r"{}\\gururmm-agent.exe", INSTALL_DIR));
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user