feat: Add Sequential Thinking to Code Review + Frontend Validation
Enhanced code review and frontend validation with intelligent triggers: Code Review Agent Enhancement: - Added Sequential Thinking MCP integration for complex issues - Triggers on 2+ rejections or 3+ critical issues - New escalation format with root cause analysis - Comprehensive solution strategies with trade-off evaluation - Educational feedback to break rejection cycles - Files: .claude/agents/code-review.md (+308 lines) - Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md Frontend Design Skill Enhancement: - Automatic invocation for ANY UI change - Comprehensive validation checklist (200+ checkpoints) - 8 validation categories (visual, interactive, responsive, a11y, etc.) - 3 validation levels (quick, standard, comprehensive) - Integration with code review workflow - Files: .claude/skills/frontend-design/SKILL.md (+120 lines) - Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines) Settings Optimization: - Repaired .claude/settings.local.json (fixed m365 pattern) - Reduced permissions from 49 to 33 (33% reduction) - Removed duplicates, sorted alphabetically - Created SETTINGS_PERMISSIONS.md documentation Checkpoint Command Enhancement: - Dual checkpoint system (git + database) - Saves session context to API for cross-machine recall - Includes git metadata in database context - Files: .claude/commands/checkpoint.md (+139 lines) Decision Rationale: - Sequential Thinking MCP breaks rejection cycles by identifying root causes - Automatic frontend validation catches UI issues before code review - Dual checkpoints enable complete project memory across machines - Settings optimization improves maintainability Total: 1,200+ lines of documentation and enhancements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,660 @@
|
||||
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(windows)]
|
||||
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<OsString>) {
|
||||
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<OsString>) -> 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→ });
|
||||
123→
|
||||
124→ // Report that we're running
|
||||
125→ status_handle
|
||||
126→ .set_service_status(ServiceStatus {
|
||||
127→ service_type: ServiceType::OWN_PROCESS,
|
||||
128→ current_state: ServiceState::Running,
|
||||
129→ controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
|
||||
130→ exit_code: ServiceExitCode::Win32(0),
|
||||
131→ checkpoint: 0,
|
||||
132→ wait_hint: Duration::default(),
|
||||
133→ process_id: None,
|
||||
134→ })
|
||||
135→ .context("Failed to set Running status")?;
|
||||
136→
|
||||
137→ // Start WebSocket client task
|
||||
138→ let ws_state = std::sync::Arc::clone(&state);
|
||||
139→ let ws_handle = tokio::spawn(async move {
|
||||
140→ loop {
|
||||
141→ info!("Connecting to server...");
|
||||
142→ match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone(
|
||||
143→ &ws_state,
|
||||
144→ ))
|
||||
145→ .await
|
||||
146→ {
|
||||
147→ Ok(_) => {
|
||||
148→ warn!("WebSocket connection closed normally, reconnecting...");
|
||||
149→ }
|
||||
150→ Err(e) => {
|
||||
151→ error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
|
||||
152→ }
|
||||
153→ }
|
||||
154→ *ws_state.connected.write().await = false;
|
||||
155→ tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||
156→ }
|
||||
157→ });
|
||||
158→
|
||||
159→ // Start metrics collection task
|
||||
160→ let metrics_state = std::sync::Arc::clone(&state);
|
||||
161→ let metrics_handle = tokio::spawn(async move {
|
||||
162→ let interval = metrics_state.config.metrics.interval_seconds;
|
||||
163→ let mut interval_timer =
|
||||
164→ tokio::time::interval(tokio::time::Duration::from_secs(interval));
|
||||
165→
|
||||
166→ loop {
|
||||
167→ interval_timer.tick().await;
|
||||
168→ let metrics = metrics_state.metrics_collector.collect().await;
|
||||
169→ if *metrics_state.connected.read().await {
|
||||
170→ info!(
|
||||
171→ "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
|
||||
172→ metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
|
||||
173→ );
|
||||
174→ }
|
||||
175→ }
|
||||
176→ });
|
||||
177→
|
||||
178→ // Wait for shutdown signal from SCM
|
||||
179→ // We use a separate task to poll the channel since it's not async
|
||||
180→ let shutdown_handle = tokio::spawn(async move {
|
||||
181→ loop {
|
||||
182→ match shutdown_rx.try_recv() {
|
||||
183→ Ok(_) => {
|
||||
184→ info!("Shutdown signal received");
|
||||
185→ break;
|
||||
186→ }
|
||||
187→ Err(mpsc::TryRecvError::Empty) => {
|
||||
188→ tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
189→ }
|
||||
190→ Err(mpsc::TryRecvError::Disconnected) => {
|
||||
191→ warn!("Shutdown channel disconnected");
|
||||
192→ break;
|
||||
193→ }
|
||||
194→ }
|
||||
195→ }
|
||||
196→ });
|
||||
197→
|
||||
198→ // Wait for shutdown
|
||||
199→ tokio::select! {
|
||||
200→ _ = shutdown_handle => {
|
||||
201→ info!("Service shutting down gracefully");
|
||||
202→ }
|
||||
203→ _ = ws_handle => {
|
||||
204→ error!("WebSocket task ended unexpectedly");
|
||||
205→ }
|
||||
206→ _ = metrics_handle => {
|
||||
207→ error!("Metrics task ended unexpectedly");
|
||||
208→ }
|
||||
209→ }
|
||||
210→
|
||||
211→ Ok::<(), anyhow::Error>(())
|
||||
212→ });
|
||||
213→
|
||||
214→ // Report that we're stopping
|
||||
215→ status_handle
|
||||
216→ .set_service_status(ServiceStatus {
|
||||
217→ service_type: ServiceType::OWN_PROCESS,
|
||||
218→ current_state: ServiceState::StopPending,
|
||||
219→ controls_accepted: ServiceControlAccept::empty(),
|
||||
220→ exit_code: ServiceExitCode::Win32(0),
|
||||
221→ checkpoint: 0,
|
||||
222→ wait_hint: Duration::from_secs(5),
|
||||
223→ process_id: None,
|
||||
224→ })
|
||||
225→ .ok();
|
||||
226→
|
||||
227→ // Report that we've stopped
|
||||
228→ status_handle
|
||||
229→ .set_service_status(ServiceStatus {
|
||||
230→ service_type: ServiceType::OWN_PROCESS,
|
||||
231→ current_state: ServiceState::Stopped,
|
||||
232→ controls_accepted: ServiceControlAccept::empty(),
|
||||
233→ exit_code: match &agent_result {
|
||||
234→ Ok(_) => ServiceExitCode::Win32(0),
|
||||
235→ Err(_) => ServiceExitCode::Win32(1),
|
||||
236→ },
|
||||
237→ checkpoint: 0,
|
||||
238→ wait_hint: Duration::default(),
|
||||
239→ process_id: None,
|
||||
240→ })
|
||||
241→ .ok();
|
||||
242→
|
||||
243→ agent_result
|
||||
244→ }
|
||||
245→
|
||||
246→ /// Known legacy service names to check and remove
|
||||
247→ const LEGACY_SERVICE_NAMES: &[&str] = &[
|
||||
248→ "GuruRMM-Agent", // NSSM-based service name
|
||||
249→ "gururmm-agent", // Alternative casing
|
||||
250→ ];
|
||||
251→
|
||||
252→ /// Detect and remove legacy service installations (e.g., NSSM-based)
|
||||
253→ fn cleanup_legacy_services() -> Result<()> {
|
||||
254→ let manager = match ServiceManager::local_computer(
|
||||
255→ None::<&str>,
|
||||
256→ ServiceManagerAccess::CONNECT,
|
||||
257→ ) {
|
||||
258→ Ok(m) => m,
|
||||
259→ Err(_) => return Ok(()), // Can't connect, skip legacy cleanup
|
||||
260→ };
|
||||
261→
|
||||
262→ for legacy_name in LEGACY_SERVICE_NAMES {
|
||||
263→ if let Ok(service) = manager.open_service(
|
||||
264→ *legacy_name,
|
||||
265→ ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE,
|
||||
266→ ) {
|
||||
267→ info!("Found legacy service '{}', removing...", legacy_name);
|
||||
268→
|
||||
269→ // Stop if running
|
||||
270→ if let Ok(status) = service.query_status() {
|
||||
271→ if status.current_state != ServiceState::Stopped {
|
||||
272→ info!("Stopping legacy service...");
|
||||
273→ let _ = service.stop();
|
||||
274→ std::thread::sleep(Duration::from_secs(3));
|
||||
275→ }
|
||||
276→ }
|
||||
277→
|
||||
278→ // Delete the service
|
||||
279→ match service.delete() {
|
||||
280→ Ok(_) => {
|
||||
281→ println!("** Removed legacy service: {}", legacy_name);
|
||||
282→ }
|
||||
283→ Err(e) => {
|
||||
284→ warn!("Failed to delete legacy service '{}': {}", legacy_name, e);
|
||||
285→ }
|
||||
286→ }
|
||||
287→ }
|
||||
288→ }
|
||||
289→
|
||||
290→ // Also check for NSSM in registry/service config
|
||||
291→ // NSSM services have specific registry keys under HKLM\SYSTEM\CurrentControlSet\Services\{name}\Parameters
|
||||
292→ for legacy_name in LEGACY_SERVICE_NAMES {
|
||||
293→ let params_key = format!(
|
||||
294→ r"SYSTEM\CurrentControlSet\Services\{}\Parameters",
|
||||
295→ legacy_name
|
||||
296→ );
|
||||
297→ // If this key exists, it was likely an NSSM service
|
||||
298→ if let Ok(output) = std::process::Command::new("reg")
|
||||
299→ .args(["query", &format!(r"HKLM\{}", params_key)])
|
||||
300→ .output()
|
||||
301→ {
|
||||
302→ if output.status.success() {
|
||||
303→ info!("Found NSSM registry keys for '{}', cleaning up...", legacy_name);
|
||||
304→ let _ = std::process::Command::new("reg")
|
||||
305→ .args(["delete", &format!(r"HKLM\{}", params_key), "/f"])
|
||||
306→ .output();
|
||||
307→ }
|
||||
308→ }
|
||||
309→ }
|
||||
310→
|
||||
311→ Ok(())
|
||||
312→ }
|
||||
313→
|
||||
314→ /// Install the agent as a Windows service using native APIs
|
||||
315→ pub fn install(
|
||||
316→ server_url: Option<String>,
|
||||
317→ api_key: Option<String>,
|
||||
318→ skip_legacy_check: bool,
|
||||
319→ ) -> Result<()> {
|
||||
320→ info!("Installing GuruRMM Agent as Windows service...");
|
||||
321→
|
||||
322→ // Clean up legacy installations unless skipped
|
||||
323→ if !skip_legacy_check {
|
||||
324→ info!("Checking for legacy service installations...");
|
||||
325→ if let Err(e) = cleanup_legacy_services() {
|
||||
326→ warn!("Legacy cleanup warning: {}", e);
|
||||
327→ }
|
||||
328→ }
|
||||
329→
|
||||
330→ // Get the current executable path
|
||||
331→ let current_exe =
|
||||
332→ std::env::current_exe().context("Failed to get current executable path")?;
|
||||
333→
|
||||
334→ let binary_dest = PathBuf::from(format!(r"{}\gururmm-agent.exe", INSTALL_DIR));
|
||||
335→ let config_dest = PathBuf::from(format!(r"{}\agent.toml", CONFIG_DIR));
|
||||
336→
|
||||
337→ // Create directories
|
||||
338→ info!("Creating directories...");
|
||||
339→ std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?;
|
||||
340→ std::fs::create_dir_all(CONFIG_DIR).context("Failed to create config directory")?;
|
||||
341→
|
||||
342→ // Copy binary
|
||||
343→ info!("Copying binary to: {:?}", binary_dest);
|
||||
344→ std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?;
|
||||
345→
|
||||
346→ // Handle configuration
|
||||
347→ let config_needs_manual_edit;
|
||||
348→ if !config_dest.exists() {
|
||||
349→ info!("Creating config: {:?}", config_dest);
|
||||
350→
|
||||
351→ // Start with sample config
|
||||
352→ let mut config = crate::config::AgentConfig::sample();
|
||||
353→
|
||||
354→ // Apply provided values
|
||||
355→ if let Some(url) = &server_url {
|
||||
356→ config.server.url = url.clone();
|
||||
357→ }
|
||||
358→ if let Some(key) = &api_key {
|
||||
359→ config.server.api_key = key.clone();
|
||||
360→ }
|
||||
361→
|
||||
362→ let toml_str = toml::to_string_pretty(&config)?;
|
||||
363→ std::fs::write(&config_dest, toml_str).context("Failed to write config file")?;
|
||||
364→
|
||||
365→ config_needs_manual_edit = server_url.is_none() || api_key.is_none();
|
||||
366→ } else {
|
||||
367→ info!("Config already exists: {:?}", config_dest);
|
||||
368→ config_needs_manual_edit = false;
|
||||
369→
|
||||
370→ // If server_url or api_key provided, update existing config
|
||||
371→ if server_url.is_some() || api_key.is_some() {
|
||||
372→ info!("Updating existing configuration...");
|
||||
373→ let config_content = std::fs::read_to_string(&config_dest)?;
|
||||
374→ let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
|
||||
375→ .context("Failed to parse existing config")?;
|
||||
376→
|
||||
377→ if let Some(url) = &server_url {
|
||||
378→ config.server.url = url.clone();
|
||||
379→ }
|
||||
380→ if let Some(key) = &api_key {
|
||||
381→ config.server.api_key = key.clone();
|
||||
382→ }
|
||||
383→
|
||||
384→ let toml_str = toml::to_string_pretty(&config)?;
|
||||
385→ std::fs::write(&config_dest, toml_str)
|
||||
386→ .context("Failed to update config file")?;
|
||||
387→ }
|
||||
388→ }
|
||||
389→
|
||||
390→ // Open the service manager
|
||||
391→ let manager = ServiceManager::local_computer(
|
||||
392→ None::<&str>,
|
||||
393→ ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
|
||||
394→ )
|
||||
395→ .context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
396→
|
||||
397→ // Check if service already exists
|
||||
398→ if let Ok(service) = manager.open_service(
|
||||
399→ SERVICE_NAME,
|
||||
400→ ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP,
|
||||
401→ ) {
|
||||
402→ info!("Removing existing service...");
|
||||
403→
|
||||
404→ // Stop the service if running
|
||||
405→ if let Ok(status) = service.query_status() {
|
||||
406→ if status.current_state != ServiceState::Stopped {
|
||||
407→ let _ = service.stop();
|
||||
408→ std::thread::sleep(Duration::from_secs(2));
|
||||
409→ }
|
||||
410→ }
|
||||
411→
|
||||
412→ // Delete the service
|
||||
413→ service.delete().context("Failed to delete existing service")?;
|
||||
414→ drop(service);
|
||||
415→
|
||||
416→ // Wait for deletion to complete
|
||||
417→ std::thread::sleep(Duration::from_secs(2));
|
||||
418→ }
|
||||
419→
|
||||
420→ // Create the service
|
||||
421→ // The service binary is called with "service" subcommand when started by SCM
|
||||
422→ let service_binary_path = format!(r#""{}" service"#, binary_dest.display());
|
||||
423→
|
||||
424→ info!("Creating service with path: {}", service_binary_path);
|
||||
425→
|
||||
426→ let service_info = ServiceInfo {
|
||||
427→ name: OsString::from(SERVICE_NAME),
|
||||
428→ display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
||||
429→ service_type: ServiceType::OWN_PROCESS,
|
||||
430→ start_type: ServiceStartType::AutoStart,
|
||||
431→ error_control: ServiceErrorControl::Normal,
|
||||
432→ executable_path: binary_dest.clone(),
|
||||
433→ launch_arguments: vec![OsString::from("service")],
|
||||
434→ dependencies: vec![],
|
||||
435→ account_name: None, // LocalSystem
|
||||
436→ account_password: None,
|
||||
437→ };
|
||||
438→
|
||||
439→ let service = manager
|
||||
440→ .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)
|
||||
441→ .context("Failed to create service")?;
|
||||
442→
|
||||
443→ // Set description
|
||||
444→ service
|
||||
445→ .set_description(SERVICE_DESCRIPTION)
|
||||
446→ .context("Failed to set service description")?;
|
||||
447→
|
||||
448→ // Configure recovery options using sc.exe (windows-service crate doesn't support this directly)
|
||||
449→ info!("Configuring recovery options...");
|
||||
450→ let _ = std::process::Command::new("sc")
|
||||
451→ .args([
|
||||
452→ "failure",
|
||||
453→ SERVICE_NAME,
|
||||
454→ "reset=86400",
|
||||
455→ "actions=restart/60000/restart/60000/restart/60000",
|
||||
456→ ])
|
||||
457→ .output();
|
||||
458→
|
||||
459→ println!("\n** GuruRMM Agent installed successfully!");
|
||||
460→ println!("\nInstalled files:");
|
||||
461→ println!(" Binary: {:?}", binary_dest);
|
||||
462→ println!(" Config: {:?}", config_dest);
|
||||
463→
|
||||
464→ if config_needs_manual_edit {
|
||||
465→ println!("\n** IMPORTANT: Edit {:?} with your server URL and API key!", config_dest);
|
||||
466→ println!("\nNext steps:");
|
||||
467→ println!(" 1. Edit {:?} with your server URL and API key", config_dest);
|
||||
468→ println!(" 2. Start the service:");
|
||||
469→ println!(" gururmm-agent start");
|
||||
470→ println!(" Or: sc start {}", SERVICE_NAME);
|
||||
471→ } else {
|
||||
472→ println!("\nStarting service...");
|
||||
473→ if let Err(e) = start() {
|
||||
474→ println!("** Failed to start service: {}. Start manually with:", e);
|
||||
475→ println!(" gururmm-agent start");
|
||||
476→ } else {
|
||||
477→ println!("** Service started successfully!");
|
||||
478→ }
|
||||
479→ }
|
||||
480→
|
||||
481→ println!("\nUseful commands:");
|
||||
482→ println!(" Status: gururmm-agent status");
|
||||
483→ println!(" Stop: gururmm-agent stop");
|
||||
484→ println!(" Start: gururmm-agent start");
|
||||
485→
|
||||
486→ Ok(())
|
||||
487→ }
|
||||
488→
|
||||
489→ /// Uninstall the Windows service
|
||||
490→ pub fn uninstall() -> Result<()> {
|
||||
491→ info!("Uninstalling GuruRMM Agent...");
|
||||
492→
|
||||
493→ let binary_path = PathBuf::from(format!(r"{}\gururmm-agent.exe", INSTALL_DIR));
|
||||
494→
|
||||
495→ // Open the service manager
|
||||
496→ let manager = ServiceManager::local_computer(
|
||||
497→ None::<&str>,
|
||||
498→ ServiceManagerAccess::CONNECT,
|
||||
499→ )
|
||||
500→ .context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
501→
|
||||
502→ // Open the service
|
||||
503→ match manager.open_service(
|
||||
504→ SERVICE_NAME,
|
||||
505→ ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE,
|
||||
506→ ) {
|
||||
507→ Ok(service) => {
|
||||
508→ // Stop if running
|
||||
509→ if let Ok(status) = service.query_status() {
|
||||
510→ if status.current_state != ServiceState::Stopped {
|
||||
511→ info!("Stopping service...");
|
||||
512→ let _ = service.stop();
|
||||
513→ std::thread::sleep(Duration::from_secs(3));
|
||||
514→ }
|
||||
515→ }
|
||||
516→
|
||||
517→ // Delete the service
|
||||
518→ info!("Deleting service...");
|
||||
519→ service.delete().context("Failed to delete service")?;
|
||||
520→ }
|
||||
521→ Err(_) => {
|
||||
522→ warn!("Service was not installed");
|
||||
523→ }
|
||||
524→ }
|
||||
525→
|
||||
526→ // Remove binary
|
||||
527→ if binary_path.exists() {
|
||||
528→ info!("Removing binary: {:?}", binary_path);
|
||||
529→ // Wait a bit for service to fully stop
|
||||
530→ std::thread::sleep(Duration::from_secs(1));
|
||||
531→ if let Err(e) = std::fs::remove_file(&binary_path) {
|
||||
532→ warn!("Failed to remove binary (may be in use): {}", e);
|
||||
533→ }
|
||||
534→ }
|
||||
535→
|
||||
536→ // Remove install directory if empty
|
||||
537→ let _ = std::fs::remove_dir(INSTALL_DIR);
|
||||
538→
|
||||
539→ println!("\n** GuruRMM Agent uninstalled successfully!");
|
||||
540→ println!(
|
||||
541→ "\nNote: Config directory {:?} was preserved.",
|
||||
542→ CONFIG_DIR
|
||||
543→ );
|
||||
544→ println!("Remove it manually if no longer needed.");
|
||||
545→
|
||||
546→ Ok(())
|
||||
547→ }
|
||||
548→
|
||||
549→ /// Start the installed service
|
||||
550→ pub fn start() -> Result<()> {
|
||||
551→ info!("Starting GuruRMM Agent service...");
|
||||
552→
|
||||
553→ let manager = ServiceManager::local_computer(
|
||||
554→ None::<&str>,
|
||||
555→ ServiceManagerAccess::CONNECT,
|
||||
556→ )
|
||||
557→ .context("Failed to connect to Service Control Manager")?;
|
||||
558→
|
||||
559→ let service = manager
|
||||
560→ .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS)
|
||||
561→ .context("Failed to open service. Is it installed?")?;
|
||||
562→
|
||||
563→ service
|
||||
564→ .start::<String>(&[])
|
||||
565→ .context("Failed to start service")?;
|
||||
566→
|
||||
567→ // Wait briefly and check status
|
||||
568→ std::thread::sleep(Duration::from_secs(2));
|
||||
569→
|
||||
570→ let status = service.query_status()?;
|
||||
571→ match status.current_state {
|
||||
572→ ServiceState::Running => {
|
||||
573→ println!("** Service started successfully");
|
||||
574→ println!("Check status: gururmm-agent status");
|
||||
575→ }
|
||||
576→ ServiceState::StartPending => {
|
||||
577→ println!("** Service is starting...");
|
||||
578→ println!("Check status: gururmm-agent status");
|
||||
579→ }
|
||||
580→ other => {
|
||||
581→ println!("Service state: {:?}", other);
|
||||
582→ }
|
||||
583→ }
|
||||
584→
|
||||
585→ Ok(())
|
||||
586→ }
|
||||
587→
|
||||
588→ /// Stop the installed service
|
||||
589→ pub fn stop() -> Result<()> {
|
||||
590→ info!("Stopping GuruRMM Agent service...");
|
||||
591→
|
||||
592→ let manager = ServiceManager::local_computer(
|
||||
593→ None::<&str>,
|
||||
594→ ServiceManagerAccess::CONNECT,
|
||||
595→ )
|
||||
596→ .context("Failed to connect to Service Control Manager")?;
|
||||
597→
|
||||
598→ let service = manager
|
||||
599→ .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS)
|
||||
600→ .context("Failed to open service. Is it installed?")?;
|
||||
601→
|
||||
602→ service.stop().context("Failed to stop service")?;
|
||||
603→
|
||||
604→ // Wait and verify
|
||||
605→ std::thread::sleep(Duration::from_secs(2));
|
||||
606→
|
||||
607→ let status = service.query_status()?;
|
||||
608→ match status.current_state {
|
||||
609→ ServiceState::Stopped => {
|
||||
610→ println!("** Service stopped successfully");
|
||||
611→ }
|
||||
612→ ServiceState::StopPending => {
|
||||
613→ println!("** Service is stopping...");
|
||||
614→ }
|
||||
615→ other => {
|
||||
616→ println!("Service state: {:?}", other);
|
||||
617→ }
|
||||
618→ }
|
||||
619→
|
||||
620→ Ok(())
|
||||
621→ }
|
||||
622→
|
||||
623→ /// Query service status
|
||||
624→ pub fn status() -> Result<()> {
|
||||
625→ let manager = ServiceManager::local_computer(
|
||||
626→ None::<&str>,
|
||||
627→ ServiceManagerAccess::CONNECT,
|
||||
628→ )
|
||||
629→ .context("Failed to connect to Service Control Manager")?;
|
||||
630→
|
||||
631→ match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) {
|
||||
632→ Ok(service) => {
|
||||
633→ let status = service.query_status()?;
|
||||
634→ println!("GuruRMM Agent Service Status");
|
||||
635→ println!("============================");
|
||||
636→ println!("Service Name: {}", SERVICE_NAME);
|
||||
637→ println!("Display Name: {}", SERVICE_DISPLAY_NAME);
|
||||
638→ println!("State: {:?}", status.current_state);
|
||||
639→ println!(
|
||||
640→ "Binary: {}\\gururmm-agent.exe",
|
||||
641→ INSTALL_DIR
|
||||
642→ );
|
||||
643→ println!("Config: {}\\agent.toml", CONFIG_DIR);
|
||||
644→ }
|
||||
645→ Err(_) => {
|
||||
646→ println!("GuruRMM Agent Service Status");
|
||||
647→ println!("============================");
|
||||
648→ println!("Status: NOT INSTALLED");
|
||||
649→ println!("\nTo install: gururmm-agent install");
|
||||
650→ }
|
||||
651→ }
|
||||
652→
|
||||
653→ Ok(())
|
||||
654→ }
|
||||
655→}
|
||||
656→
|
||||
|
||||
<system-reminder>
|
||||
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.
|
||||
</system-reminder>
|
||||
@@ -0,0 +1,695 @@
|
||||
1→//! GuruRMM Agent - Cross-platform Remote Monitoring and Management Agent
|
||||
2→//!
|
||||
3→//! This agent connects to the GuruRMM server, reports system metrics,
|
||||
4→//! monitors services (watchdog), and executes remote commands.
|
||||
5→
|
||||
6→mod config;
|
||||
7→mod device_id;
|
||||
8→mod metrics;
|
||||
9→mod service;
|
||||
10→mod transport;
|
||||
11→mod updater;
|
||||
12→
|
||||
13→use anyhow::{Context, Result};
|
||||
14→use clap::{Parser, Subcommand};
|
||||
15→use std::path::PathBuf;
|
||||
16→use std::sync::Arc;
|
||||
17→use tokio::sync::RwLock;
|
||||
18→use tracing::{error, info, warn};
|
||||
19→
|
||||
20→use crate::config::AgentConfig;
|
||||
21→use crate::metrics::MetricsCollector;
|
||||
22→use crate::transport::WebSocketClient;
|
||||
23→
|
||||
24→/// GuruRMM Agent - Remote Monitoring and Management
|
||||
25→#[derive(Parser)]
|
||||
26→#[command(name = "gururmm-agent")]
|
||||
27→#[command(author, version, about, long_about = None)]
|
||||
28→struct Cli {
|
||||
29→ /// Path to configuration file
|
||||
30→ #[arg(short, long, default_value = "agent.toml")]
|
||||
31→ config: PathBuf,
|
||||
32→
|
||||
33→ /// Subcommand to run
|
||||
34→ #[command(subcommand)]
|
||||
35→ command: Option<Commands>,
|
||||
36→}
|
||||
37→
|
||||
38→#[derive(Subcommand)]
|
||||
39→enum Commands {
|
||||
40→ /// Run the agent (default)
|
||||
41→ Run,
|
||||
42→
|
||||
43→ /// Install as a system service
|
||||
44→ Install {
|
||||
45→ /// Server WebSocket URL (e.g., wss://rmm-api.example.com/ws)
|
||||
46→ #[arg(long)]
|
||||
47→ server_url: Option<String>,
|
||||
48→
|
||||
49→ /// API key for authentication
|
||||
50→ #[arg(long)]
|
||||
51→ api_key: Option<String>,
|
||||
52→
|
||||
53→ /// Skip legacy service detection and cleanup
|
||||
54→ #[arg(long, default_value = "false")]
|
||||
55→ skip_legacy_check: bool,
|
||||
56→ },
|
||||
57→
|
||||
58→ /// Uninstall the system service
|
||||
59→ Uninstall,
|
||||
60→
|
||||
61→ /// Start the installed service
|
||||
62→ Start,
|
||||
63→
|
||||
64→ /// Stop the installed service
|
||||
65→ Stop,
|
||||
66→
|
||||
67→ /// Show agent status
|
||||
68→ Status,
|
||||
69→
|
||||
70→ /// Generate a sample configuration file
|
||||
71→ GenerateConfig {
|
||||
72→ /// Output path for config file
|
||||
73→ #[arg(short, long, default_value = "agent.toml")]
|
||||
74→ output: PathBuf,
|
||||
75→ },
|
||||
76→
|
||||
77→ /// Run as Windows service (called by SCM, not for manual use)
|
||||
78→ #[command(hide = true)]
|
||||
79→ Service,
|
||||
80→}
|
||||
81→
|
||||
82→/// Shared application state
|
||||
83→pub struct AppState {
|
||||
84→ pub config: AgentConfig,
|
||||
85→ pub metrics_collector: MetricsCollector,
|
||||
86→ pub connected: RwLock<bool>,
|
||||
87→}
|
||||
88→
|
||||
89→#[tokio::main]
|
||||
90→async fn main() -> Result<()> {
|
||||
91→ // Initialize logging
|
||||
92→ tracing_subscriber::fmt()
|
||||
93→ .with_env_filter(
|
||||
94→ tracing_subscriber::EnvFilter::from_default_env()
|
||||
95→ .add_directive("gururmm_agent=info".parse()?)
|
||||
96→ .add_directive("info".parse()?),
|
||||
97→ )
|
||||
98→ .init();
|
||||
99→
|
||||
100→ let cli = Cli::parse();
|
||||
101→
|
||||
102→ match cli.command.unwrap_or(Commands::Run) {
|
||||
103→ Commands::Run => run_agent(cli.config).await,
|
||||
104→ Commands::Install { server_url, api_key, skip_legacy_check } => {
|
||||
105→ install_service(server_url, api_key, skip_legacy_check).await
|
||||
106→ }
|
||||
107→ Commands::Uninstall => uninstall_service().await,
|
||||
108→ Commands::Start => start_service().await,
|
||||
109→ Commands::Stop => stop_service().await,
|
||||
110→ Commands::Status => show_status(cli.config).await,
|
||||
111→ Commands::GenerateConfig { output } => generate_config(output).await,
|
||||
112→ Commands::Service => run_as_windows_service(),
|
||||
113→ }
|
||||
114→}
|
||||
115→
|
||||
116→/// Run as a Windows service (called by SCM)
|
||||
117→fn run_as_windows_service() -> Result<()> {
|
||||
118→ #[cfg(windows)]
|
||||
119→ {
|
||||
120→ service::windows::run_as_service()
|
||||
121→ }
|
||||
122→
|
||||
123→ #[cfg(not(windows))]
|
||||
124→ {
|
||||
125→ anyhow::bail!("Windows service mode is only available on Windows");
|
||||
126→ }
|
||||
127→}
|
||||
128→
|
||||
129→/// Main agent runtime loop
|
||||
130→async fn run_agent(config_path: PathBuf) -> Result<()> {
|
||||
131→ info!("GuruRMM Agent starting...");
|
||||
132→
|
||||
133→ // Load configuration
|
||||
134→ let config = AgentConfig::load(&config_path)?;
|
||||
135→ info!("Loaded configuration from {:?}", config_path);
|
||||
136→ info!("Server URL: {}", config.server.url);
|
||||
137→
|
||||
138→ // Initialize metrics collector
|
||||
139→ let metrics_collector = MetricsCollector::new();
|
||||
140→ info!("Metrics collector initialized");
|
||||
141→
|
||||
142→ // Create shared state
|
||||
143→ let state = Arc::new(AppState {
|
||||
144→ config: config.clone(),
|
||||
145→ metrics_collector,
|
||||
146→ connected: RwLock::new(false),
|
||||
147→ });
|
||||
148→
|
||||
149→ // Start the WebSocket client with auto-reconnect
|
||||
150→ let ws_state = Arc::clone(&state);
|
||||
151→ let ws_handle = tokio::spawn(async move {
|
||||
152→ loop {
|
||||
153→ info!("Connecting to server...");
|
||||
154→ match WebSocketClient::connect_and_run(Arc::clone(&ws_state)).await {
|
||||
155→ Ok(_) => {
|
||||
156→ warn!("WebSocket connection closed normally, reconnecting...");
|
||||
157→ }
|
||||
158→ Err(e) => {
|
||||
159→ error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
|
||||
160→ }
|
||||
161→ }
|
||||
162→
|
||||
163→ // Mark as disconnected
|
||||
164→ *ws_state.connected.write().await = false;
|
||||
165→
|
||||
166→ // Wait before reconnecting
|
||||
167→ tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||
168→ }
|
||||
169→ });
|
||||
170→
|
||||
171→ // Start metrics collection loop
|
||||
172→ let metrics_state = Arc::clone(&state);
|
||||
173→ let metrics_handle = tokio::spawn(async move {
|
||||
174→ let interval = metrics_state.config.metrics.interval_seconds;
|
||||
175→ let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval));
|
||||
176→
|
||||
177→ loop {
|
||||
178→ interval_timer.tick().await;
|
||||
179→
|
||||
180→ // Collect metrics (they'll be sent via WebSocket if connected)
|
||||
181→ let metrics = metrics_state.metrics_collector.collect().await;
|
||||
182→ if *metrics_state.connected.read().await {
|
||||
183→ info!(
|
||||
184→ "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
|
||||
185→ metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
|
||||
186→ );
|
||||
187→ }
|
||||
188→ }
|
||||
189→ });
|
||||
190→
|
||||
191→ // Wait for shutdown signal
|
||||
192→ tokio::select! {
|
||||
193→ _ = tokio::signal::ctrl_c() => {
|
||||
194→ info!("Received shutdown signal");
|
||||
195→ }
|
||||
196→ _ = ws_handle => {
|
||||
197→ error!("WebSocket task ended unexpectedly");
|
||||
198→ }
|
||||
199→ _ = metrics_handle => {
|
||||
200→ error!("Metrics task ended unexpectedly");
|
||||
201→ }
|
||||
202→ }
|
||||
203→
|
||||
204→ info!("GuruRMM Agent shutting down");
|
||||
205→ Ok(())
|
||||
206→}
|
||||
207→
|
||||
208→/// Install the agent as a system service
|
||||
209→async fn install_service(
|
||||
210→ server_url: Option<String>,
|
||||
211→ api_key: Option<String>,
|
||||
212→ skip_legacy_check: bool,
|
||||
213→) -> Result<()> {
|
||||
214→ #[cfg(windows)]
|
||||
215→ {
|
||||
216→ service::windows::install(server_url, api_key, skip_legacy_check)
|
||||
217→ }
|
||||
218→
|
||||
219→ #[cfg(target_os = "linux")]
|
||||
220→ {
|
||||
221→ install_systemd_service(server_url, api_key, skip_legacy_check).await
|
||||
222→ }
|
||||
223→
|
||||
224→ #[cfg(target_os = "macos")]
|
||||
225→ {
|
||||
226→ let _ = (server_url, api_key, skip_legacy_check); // Suppress unused warnings
|
||||
227→ info!("Installing GuruRMM Agent as launchd service...");
|
||||
228→ todo!("macOS launchd service installation not yet implemented");
|
||||
229→ }
|
||||
230→}
|
||||
231→
|
||||
232→/// Legacy service names to check for and clean up (Linux)
|
||||
233→#[cfg(target_os = "linux")]
|
||||
234→const LINUX_LEGACY_SERVICE_NAMES: &[&str] = &[
|
||||
235→ "gururmm", // Old name without -agent suffix
|
||||
236→ "guru-rmm-agent", // Alternative naming
|
||||
237→ "GuruRMM-Agent", // Case variant
|
||||
238→];
|
||||
239→
|
||||
240→/// Clean up legacy Linux service installations
|
||||
241→#[cfg(target_os = "linux")]
|
||||
242→fn cleanup_legacy_linux_services() -> Result<()> {
|
||||
243→ use std::process::Command;
|
||||
244→
|
||||
245→ info!("Checking for legacy service installations...");
|
||||
246→
|
||||
247→ for legacy_name in LINUX_LEGACY_SERVICE_NAMES {
|
||||
248→ // Check if service exists
|
||||
249→ let status = Command::new("systemctl")
|
||||
250→ .args(["status", legacy_name])
|
||||
251→ .output();
|
||||
252→
|
||||
253→ if let Ok(output) = status {
|
||||
254→ if output.status.success() || String::from_utf8_lossy(&output.stderr).contains("Loaded:") {
|
||||
255→ info!("Found legacy service '{}', removing...", legacy_name);
|
||||
256→
|
||||
257→ // Stop the service
|
||||
258→ let _ = Command::new("systemctl")
|
||||
259→ .args(["stop", legacy_name])
|
||||
260→ .status();
|
||||
261→
|
||||
262→ // Disable the service
|
||||
263→ let _ = Command::new("systemctl")
|
||||
264→ .args(["disable", legacy_name])
|
||||
265→ .status();
|
||||
266→
|
||||
267→ // Remove unit file
|
||||
268→ let unit_file = format!("/etc/systemd/system/{}.service", legacy_name);
|
||||
269→ if std::path::Path::new(&unit_file).exists() {
|
||||
270→ info!("Removing legacy unit file: {}", unit_file);
|
||||
271→ let _ = std::fs::remove_file(&unit_file);
|
||||
272→ }
|
||||
273→ }
|
||||
274→ }
|
||||
275→ }
|
||||
276→
|
||||
277→ // Check for legacy binaries in common locations
|
||||
278→ let legacy_binary_locations = [
|
||||
279→ "/usr/local/bin/gururmm",
|
||||
280→ "/usr/bin/gururmm",
|
||||
281→ "/opt/gururmm/gururmm",
|
||||
282→ "/opt/gururmm/agent",
|
||||
283→ ];
|
||||
284→
|
||||
285→ for legacy_path in legacy_binary_locations {
|
||||
286→ if std::path::Path::new(legacy_path).exists() {
|
||||
287→ info!("Found legacy binary at '{}', removing...", legacy_path);
|
||||
288→ let _ = std::fs::remove_file(legacy_path);
|
||||
289→ }
|
||||
290→ }
|
||||
291→
|
||||
292→ // Reload systemd to pick up removed unit files
|
||||
293→ let _ = Command::new("systemctl")
|
||||
294→ .args(["daemon-reload"])
|
||||
295→ .status();
|
||||
296→
|
||||
297→ Ok(())
|
||||
298→}
|
||||
299→
|
||||
300→/// Install as a systemd service (Linux)
|
||||
301→#[cfg(target_os = "linux")]
|
||||
302→async fn install_systemd_service(
|
||||
303→ server_url: Option<String>,
|
||||
304→ api_key: Option<String>,
|
||||
305→ skip_legacy_check: bool,
|
||||
306→) -> Result<()> {
|
||||
307→ use std::process::Command;
|
||||
308→
|
||||
309→ const SERVICE_NAME: &str = "gururmm-agent";
|
||||
310→ const INSTALL_DIR: &str = "/usr/local/bin";
|
||||
311→ const CONFIG_DIR: &str = "/etc/gururmm";
|
||||
312→ const SYSTEMD_DIR: &str = "/etc/systemd/system";
|
||||
313→
|
||||
314→ info!("Installing GuruRMM Agent as systemd service...");
|
||||
315→
|
||||
316→ // Check if running as root
|
||||
317→ if !nix::unistd::geteuid().is_root() {
|
||||
318→ anyhow::bail!("Installation requires root privileges. Please run with sudo.");
|
||||
319→ }
|
||||
320→
|
||||
321→ // Clean up legacy installations unless skipped
|
||||
322→ if !skip_legacy_check {
|
||||
323→ if let Err(e) = cleanup_legacy_linux_services() {
|
||||
324→ warn!("Legacy cleanup warning: {}", e);
|
||||
325→ }
|
||||
326→ }
|
||||
327→
|
||||
328→ // Get the current executable path
|
||||
329→ let current_exe = std::env::current_exe()
|
||||
330→ .context("Failed to get current executable path")?;
|
||||
331→
|
||||
332→ let binary_dest = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
|
||||
333→ let config_dest = format!("{}/agent.toml", CONFIG_DIR);
|
||||
334→ let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);
|
||||
335→
|
||||
336→ // Create config directory
|
||||
337→ info!("Creating config directory: {}", CONFIG_DIR);
|
||||
338→ std::fs::create_dir_all(CONFIG_DIR)
|
||||
339→ .context("Failed to create config directory")?;
|
||||
340→
|
||||
341→ // Copy binary
|
||||
342→ info!("Copying binary to: {}", binary_dest);
|
||||
343→ std::fs::copy(¤t_exe, &binary_dest)
|
||||
344→ .context("Failed to copy binary")?;
|
||||
345→
|
||||
346→ // Make binary executable
|
||||
347→ Command::new("chmod")
|
||||
348→ .args(["+x", &binary_dest])
|
||||
349→ .status()
|
||||
350→ .context("Failed to set binary permissions")?;
|
||||
351→
|
||||
352→ // Handle configuration
|
||||
353→ let config_needs_manual_edit;
|
||||
354→ if !std::path::Path::new(&config_dest).exists() {
|
||||
355→ info!("Creating config: {}", config_dest);
|
||||
356→
|
||||
357→ // Start with sample config
|
||||
358→ let mut config = crate::config::AgentConfig::sample();
|
||||
359→
|
||||
360→ // Apply provided values
|
||||
361→ if let Some(url) = &server_url {
|
||||
362→ config.server.url = url.clone();
|
||||
363→ }
|
||||
364→ if let Some(key) = &api_key {
|
||||
365→ config.server.api_key = key.clone();
|
||||
366→ }
|
||||
367→
|
||||
368→ let toml_str = toml::to_string_pretty(&config)?;
|
||||
369→ std::fs::write(&config_dest, toml_str)
|
||||
370→ .context("Failed to write config file")?;
|
||||
371→
|
||||
372→ // Set restrictive permissions on config (contains API key)
|
||||
373→ Command::new("chmod")
|
||||
374→ .args(["600", &config_dest])
|
||||
375→ .status()
|
||||
376→ .context("Failed to set config permissions")?;
|
||||
377→
|
||||
378→ config_needs_manual_edit = server_url.is_none() || api_key.is_none();
|
||||
379→ } else {
|
||||
380→ info!("Config already exists: {}", config_dest);
|
||||
381→ config_needs_manual_edit = false;
|
||||
382→
|
||||
383→ // If server_url or api_key provided, update existing config
|
||||
384→ if server_url.is_some() || api_key.is_some() {
|
||||
385→ info!("Updating existing configuration...");
|
||||
386→ let config_content = std::fs::read_to_string(&config_dest)?;
|
||||
387→ let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
|
||||
388→ .context("Failed to parse existing config")?;
|
||||
389→
|
||||
390→ if let Some(url) = &server_url {
|
||||
391→ config.server.url = url.clone();
|
||||
392→ }
|
||||
393→ if let Some(key) = &api_key {
|
||||
394→ config.server.api_key = key.clone();
|
||||
395→ }
|
||||
396→
|
||||
397→ let toml_str = toml::to_string_pretty(&config)?;
|
||||
398→ std::fs::write(&config_dest, toml_str)
|
||||
399→ .context("Failed to update config file")?;
|
||||
400→ }
|
||||
401→ }
|
||||
402→
|
||||
403→ // Create systemd unit file
|
||||
404→ let unit_content = format!(r#"[Unit]
|
||||
405→Description=GuruRMM Agent - Remote Monitoring and Management
|
||||
406→Documentation=https://github.com/azcomputerguru/gururmm
|
||||
407→After=network-online.target
|
||||
408→Wants=network-online.target
|
||||
409→
|
||||
410→[Service]
|
||||
411→Type=simple
|
||||
412→ExecStart={binary} --config {config} run
|
||||
413→Restart=always
|
||||
414→RestartSec=10
|
||||
415→StandardOutput=journal
|
||||
416→StandardError=journal
|
||||
417→SyslogIdentifier={service}
|
||||
418→
|
||||
419→# Security hardening
|
||||
420→NoNewPrivileges=true
|
||||
421→ProtectSystem=strict
|
||||
422→ProtectHome=read-only
|
||||
423→PrivateTmp=true
|
||||
424→ReadWritePaths=/var/log
|
||||
425→
|
||||
426→[Install]
|
||||
427→WantedBy=multi-user.target
|
||||
428→"#,
|
||||
429→ binary = binary_dest,
|
||||
430→ config = config_dest,
|
||||
431→ service = SERVICE_NAME
|
||||
432→ );
|
||||
433→
|
||||
434→ info!("Creating systemd unit file: {}", unit_file);
|
||||
435→ std::fs::write(&unit_file, unit_content)
|
||||
436→ .context("Failed to write systemd unit file")?;
|
||||
437→
|
||||
438→ // Reload systemd daemon
|
||||
439→ info!("Reloading systemd daemon...");
|
||||
440→ let status = Command::new("systemctl")
|
||||
441→ .args(["daemon-reload"])
|
||||
442→ .status()
|
||||
443→ .context("Failed to reload systemd")?;
|
||||
444→
|
||||
445→ if !status.success() {
|
||||
446→ anyhow::bail!("systemctl daemon-reload failed");
|
||||
447→ }
|
||||
448→
|
||||
449→ // Enable the service
|
||||
450→ info!("Enabling service...");
|
||||
451→ let status = Command::new("systemctl")
|
||||
452→ .args(["enable", SERVICE_NAME])
|
||||
453→ .status()
|
||||
454→ .context("Failed to enable service")?;
|
||||
455→
|
||||
456→ if !status.success() {
|
||||
457→ anyhow::bail!("systemctl enable failed");
|
||||
458→ }
|
||||
459→
|
||||
460→ println!("\n✓ GuruRMM Agent installed successfully!");
|
||||
461→ println!("\nInstalled files:");
|
||||
462→ println!(" Binary: {}", binary_dest);
|
||||
463→ println!(" Config: {}", config_dest);
|
||||
464→ println!(" Service: {}", unit_file);
|
||||
465→
|
||||
466→ if config_needs_manual_edit {
|
||||
467→ println!("\n⚠️ IMPORTANT: Edit {} with your server URL and API key!", config_dest);
|
||||
468→ println!("\nNext steps:");
|
||||
469→ println!(" 1. Edit {} with your server URL and API key", config_dest);
|
||||
470→ println!(" 2. Start the service: sudo systemctl start {}", SERVICE_NAME);
|
||||
471→ } else {
|
||||
472→ println!("\nStarting service...");
|
||||
473→ let status = Command::new("systemctl")
|
||||
474→ .args(["start", SERVICE_NAME])
|
||||
475→ .status();
|
||||
476→
|
||||
477→ if status.is_ok() && status.unwrap().success() {
|
||||
478→ println!("✓ Service started successfully!");
|
||||
479→ } else {
|
||||
480→ println!("⚠️ Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
|
||||
481→ }
|
||||
482→ }
|
||||
483→
|
||||
484→ println!("\nUseful commands:");
|
||||
485→ println!(" Status: sudo systemctl status {}", SERVICE_NAME);
|
||||
486→ println!(" Logs: sudo journalctl -u {} -f", SERVICE_NAME);
|
||||
487→ println!(" Stop: sudo systemctl stop {}", SERVICE_NAME);
|
||||
488→ println!(" Start: sudo systemctl start {}", SERVICE_NAME);
|
||||
489→
|
||||
490→ Ok(())
|
||||
491→}
|
||||
492→
|
||||
493→/// Uninstall the system service
|
||||
494→async fn uninstall_service() -> Result<()> {
|
||||
495→ #[cfg(windows)]
|
||||
496→ {
|
||||
497→ service::windows::uninstall()
|
||||
498→ }
|
||||
499→
|
||||
500→ #[cfg(target_os = "linux")]
|
||||
501→ {
|
||||
502→ uninstall_systemd_service().await
|
||||
503→ }
|
||||
504→
|
||||
505→ #[cfg(target_os = "macos")]
|
||||
506→ {
|
||||
507→ todo!("macOS service uninstallation not yet implemented");
|
||||
508→ }
|
||||
509→}
|
||||
510→
|
||||
511→/// Uninstall systemd service (Linux)
|
||||
512→#[cfg(target_os = "linux")]
|
||||
513→async fn uninstall_systemd_service() -> Result<()> {
|
||||
514→ use std::process::Command;
|
||||
515→
|
||||
516→ const SERVICE_NAME: &str = "gururmm-agent";
|
||||
517→ const INSTALL_DIR: &str = "/usr/local/bin";
|
||||
518→ const CONFIG_DIR: &str = "/etc/gururmm";
|
||||
519→ const SYSTEMD_DIR: &str = "/etc/systemd/system";
|
||||
520→
|
||||
521→ info!("Uninstalling GuruRMM Agent...");
|
||||
522→
|
||||
523→ if !nix::unistd::geteuid().is_root() {
|
||||
524→ anyhow::bail!("Uninstallation requires root privileges. Please run with sudo.");
|
||||
525→ }
|
||||
526→
|
||||
527→ let binary_path = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
|
||||
528→ let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);
|
||||
529→
|
||||
530→ // Stop the service if running
|
||||
531→ info!("Stopping service...");
|
||||
532→ let _ = Command::new("systemctl")
|
||||
533→ .args(["stop", SERVICE_NAME])
|
||||
534→ .status();
|
||||
535→
|
||||
536→ // Disable the service
|
||||
537→ info!("Disabling service...");
|
||||
538→ let _ = Command::new("systemctl")
|
||||
539→ .args(["disable", SERVICE_NAME])
|
||||
540→ .status();
|
||||
541→
|
||||
542→ // Remove unit file
|
||||
543→ if std::path::Path::new(&unit_file).exists() {
|
||||
544→ info!("Removing unit file: {}", unit_file);
|
||||
545→ std::fs::remove_file(&unit_file)?;
|
||||
546→ }
|
||||
547→
|
||||
548→ // Remove binary
|
||||
549→ if std::path::Path::new(&binary_path).exists() {
|
||||
550→ info!("Removing binary: {}", binary_path);
|
||||
551→ std::fs::remove_file(&binary_path)?;
|
||||
552→ }
|
||||
553→
|
||||
554→ // Reload systemd
|
||||
555→ let _ = Command::new("systemctl")
|
||||
556→ .args(["daemon-reload"])
|
||||
557→ .status();
|
||||
558→
|
||||
559→ println!("\n✓ GuruRMM Agent uninstalled successfully!");
|
||||
560→ println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
|
||||
561→ println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);
|
||||
562→
|
||||
563→ Ok(())
|
||||
564→}
|
||||
565→
|
||||
566→/// Start the installed service
|
||||
567→async fn start_service() -> Result<()> {
|
||||
568→ #[cfg(windows)]
|
||||
569→ {
|
||||
570→ service::windows::start()
|
||||
571→ }
|
||||
572→
|
||||
573→ #[cfg(target_os = "linux")]
|
||||
574→ {
|
||||
575→ use std::process::Command;
|
||||
576→
|
||||
577→ info!("Starting GuruRMM Agent service...");
|
||||
578→
|
||||
579→ let status = Command::new("systemctl")
|
||||
580→ .args(["start", "gururmm-agent"])
|
||||
581→ .status()
|
||||
582→ .context("Failed to start service")?;
|
||||
583→
|
||||
584→ if status.success() {
|
||||
585→ println!("** Service started successfully");
|
||||
586→ println!("Check status: sudo systemctl status gururmm-agent");
|
||||
587→ } else {
|
||||
588→ anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
|
||||
589→ }
|
||||
590→
|
||||
591→ Ok(())
|
||||
592→ }
|
||||
593→
|
||||
594→ #[cfg(target_os = "macos")]
|
||||
595→ {
|
||||
596→ todo!("macOS service start not yet implemented");
|
||||
597→ }
|
||||
598→}
|
||||
599→
|
||||
600→/// Stop the installed service
|
||||
601→async fn stop_service() -> Result<()> {
|
||||
602→ #[cfg(windows)]
|
||||
603→ {
|
||||
604→ service::windows::stop()
|
||||
605→ }
|
||||
606→
|
||||
607→ #[cfg(target_os = "linux")]
|
||||
608→ {
|
||||
609→ use std::process::Command;
|
||||
610→
|
||||
611→ info!("Stopping GuruRMM Agent service...");
|
||||
612→
|
||||
613→ let status = Command::new("systemctl")
|
||||
614→ .args(["stop", "gururmm-agent"])
|
||||
615→ .status()
|
||||
616→ .context("Failed to stop service")?;
|
||||
617→
|
||||
618→ if status.success() {
|
||||
619→ println!("** Service stopped successfully");
|
||||
620→ } else {
|
||||
621→ anyhow::bail!("Failed to stop service");
|
||||
622→ }
|
||||
623→
|
||||
624→ Ok(())
|
||||
625→ }
|
||||
626→
|
||||
627→ #[cfg(target_os = "macos")]
|
||||
628→ {
|
||||
629→ todo!("macOS service stop not yet implemented");
|
||||
630→ }
|
||||
631→}
|
||||
632→
|
||||
633→/// Show agent status
|
||||
634→async fn show_status(config_path: PathBuf) -> Result<()> {
|
||||
635→ // On Windows, show service status
|
||||
636→ #[cfg(windows)]
|
||||
637→ {
|
||||
638→ service::windows::status()?;
|
||||
639→ println!();
|
||||
640→ }
|
||||
641→
|
||||
642→ // Try to load config for additional info
|
||||
643→ match AgentConfig::load(&config_path) {
|
||||
644→ Ok(config) => {
|
||||
645→ println!("Configuration");
|
||||
646→ println!("=============");
|
||||
647→ println!("Config file: {:?}", config_path);
|
||||
648→ println!("Server URL: {}", config.server.url);
|
||||
649→ println!("Metrics interval: {} seconds", config.metrics.interval_seconds);
|
||||
650→ println!("Watchdog enabled: {}", config.watchdog.enabled);
|
||||
651→
|
||||
652→ // Collect current metrics
|
||||
653→ let collector = MetricsCollector::new();
|
||||
654→ let metrics = collector.collect().await;
|
||||
655→
|
||||
656→ println!("\nCurrent System Metrics:");
|
||||
657→ println!(" CPU Usage: {:.1}%", metrics.cpu_percent);
|
||||
658→ println!(" Memory Usage: {:.1}%", metrics.memory_percent);
|
||||
659→ println!(
|
||||
660→ " Memory Used: {:.2} GB",
|
||||
661→ metrics.memory_used_bytes as f64 / 1_073_741_824.0
|
||||
662→ );
|
||||
663→ println!(" Disk Usage: {:.1}%", metrics.disk_percent);
|
||||
664→ println!(
|
||||
665→ " Disk Used: {:.2} GB",
|
||||
666→ metrics.disk_used_bytes as f64 / 1_073_741_824.0
|
||||
667→ );
|
||||
668→ }
|
||||
669→ Err(_) => {
|
||||
670→ println!("\nConfig file {:?} not found or invalid.", config_path);
|
||||
671→ #[cfg(windows)]
|
||||
672→ println!("Service config location: {}\\agent.toml", service::windows::CONFIG_DIR);
|
||||
673→ }
|
||||
674→ }
|
||||
675→
|
||||
676→ Ok(())
|
||||
677→}
|
||||
678→
|
||||
679→/// Generate a sample configuration file
|
||||
680→async fn generate_config(output: PathBuf) -> Result<()> {
|
||||
681→ let sample_config = AgentConfig::sample();
|
||||
682→ let toml_str = toml::to_string_pretty(&sample_config)?;
|
||||
683→
|
||||
684→ std::fs::write(&output, toml_str)?;
|
||||
685→ println!("Sample configuration written to {:?}", output);
|
||||
686→ println!("\nEdit this file with your server URL and API key, then run:");
|
||||
687→ println!(" gururmm-agent --config {:?} run", output);
|
||||
688→
|
||||
689→ Ok(())
|
||||
690→}
|
||||
691→
|
||||
|
||||
<system-reminder>
|
||||
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.
|
||||
</system-reminder>
|
||||
@@ -0,0 +1,660 @@
|
||||
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(windows)]
|
||||
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<OsString>) {
|
||||
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<OsString>) -> 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→ });
|
||||
123→
|
||||
124→ // Report that we're running
|
||||
125→ status_handle
|
||||
126→ .set_service_status(ServiceStatus {
|
||||
127→ service_type: ServiceType::OWN_PROCESS,
|
||||
128→ current_state: ServiceState::Running,
|
||||
129→ controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
|
||||
130→ exit_code: ServiceExitCode::Win32(0),
|
||||
131→ checkpoint: 0,
|
||||
132→ wait_hint: Duration::default(),
|
||||
133→ process_id: None,
|
||||
134→ })
|
||||
135→ .context("Failed to set Running status")?;
|
||||
136→
|
||||
137→ // Start WebSocket client task
|
||||
138→ let ws_state = std::sync::Arc::clone(&state);
|
||||
139→ let ws_handle = tokio::spawn(async move {
|
||||
140→ loop {
|
||||
141→ info!("Connecting to server...");
|
||||
142→ match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone(
|
||||
143→ &ws_state,
|
||||
144→ ))
|
||||
145→ .await
|
||||
146→ {
|
||||
147→ Ok(_) => {
|
||||
148→ warn!("WebSocket connection closed normally, reconnecting...");
|
||||
149→ }
|
||||
150→ Err(e) => {
|
||||
151→ error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
|
||||
152→ }
|
||||
153→ }
|
||||
154→ *ws_state.connected.write().await = false;
|
||||
155→ tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||
156→ }
|
||||
157→ });
|
||||
158→
|
||||
159→ // Start metrics collection task
|
||||
160→ let metrics_state = std::sync::Arc::clone(&state);
|
||||
161→ let metrics_handle = tokio::spawn(async move {
|
||||
162→ let interval = metrics_state.config.metrics.interval_seconds;
|
||||
163→ let mut interval_timer =
|
||||
164→ tokio::time::interval(tokio::time::Duration::from_secs(interval));
|
||||
165→
|
||||
166→ loop {
|
||||
167→ interval_timer.tick().await;
|
||||
168→ let metrics = metrics_state.metrics_collector.collect().await;
|
||||
169→ if *metrics_state.connected.read().await {
|
||||
170→ info!(
|
||||
171→ "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
|
||||
172→ metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
|
||||
173→ );
|
||||
174→ }
|
||||
175→ }
|
||||
176→ });
|
||||
177→
|
||||
178→ // Wait for shutdown signal from SCM
|
||||
179→ // We use a separate task to poll the channel since it's not async
|
||||
180→ let shutdown_handle = tokio::spawn(async move {
|
||||
181→ loop {
|
||||
182→ match shutdown_rx.try_recv() {
|
||||
183→ Ok(_) => {
|
||||
184→ info!("Shutdown signal received");
|
||||
185→ break;
|
||||
186→ }
|
||||
187→ Err(mpsc::TryRecvError::Empty) => {
|
||||
188→ tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
189→ }
|
||||
190→ Err(mpsc::TryRecvError::Disconnected) => {
|
||||
191→ warn!("Shutdown channel disconnected");
|
||||
192→ break;
|
||||
193→ }
|
||||
194→ }
|
||||
195→ }
|
||||
196→ });
|
||||
197→
|
||||
198→ // Wait for shutdown
|
||||
199→ tokio::select! {
|
||||
200→ _ = shutdown_handle => {
|
||||
201→ info!("Service shutting down gracefully");
|
||||
202→ }
|
||||
203→ _ = ws_handle => {
|
||||
204→ error!("WebSocket task ended unexpectedly");
|
||||
205→ }
|
||||
206→ _ = metrics_handle => {
|
||||
207→ error!("Metrics task ended unexpectedly");
|
||||
208→ }
|
||||
209→ }
|
||||
210→
|
||||
211→ Ok::<(), anyhow::Error>(())
|
||||
212→ });
|
||||
213→
|
||||
214→ // Report that we're stopping
|
||||
215→ status_handle
|
||||
216→ .set_service_status(ServiceStatus {
|
||||
217→ service_type: ServiceType::OWN_PROCESS,
|
||||
218→ current_state: ServiceState::StopPending,
|
||||
219→ controls_accepted: ServiceControlAccept::empty(),
|
||||
220→ exit_code: ServiceExitCode::Win32(0),
|
||||
221→ checkpoint: 0,
|
||||
222→ wait_hint: Duration::from_secs(5),
|
||||
223→ process_id: None,
|
||||
224→ })
|
||||
225→ .ok();
|
||||
226→
|
||||
227→ // Report that we've stopped
|
||||
228→ status_handle
|
||||
229→ .set_service_status(ServiceStatus {
|
||||
230→ service_type: ServiceType::OWN_PROCESS,
|
||||
231→ current_state: ServiceState::Stopped,
|
||||
232→ controls_accepted: ServiceControlAccept::empty(),
|
||||
233→ exit_code: match &agent_result {
|
||||
234→ Ok(_) => ServiceExitCode::Win32(0),
|
||||
235→ Err(_) => ServiceExitCode::Win32(1),
|
||||
236→ },
|
||||
237→ checkpoint: 0,
|
||||
238→ wait_hint: Duration::default(),
|
||||
239→ process_id: None,
|
||||
240→ })
|
||||
241→ .ok();
|
||||
242→
|
||||
243→ agent_result
|
||||
244→ }
|
||||
245→
|
||||
246→ /// Known legacy service names to check and remove
|
||||
247→ const LEGACY_SERVICE_NAMES: &[&str] = &[
|
||||
248→ "GuruRMM-Agent", // NSSM-based service name
|
||||
249→ "gururmm-agent", // Alternative casing
|
||||
250→ ];
|
||||
251→
|
||||
252→ /// Detect and remove legacy service installations (e.g., NSSM-based)
|
||||
253→ fn cleanup_legacy_services() -> Result<()> {
|
||||
254→ let manager = match ServiceManager::local_computer(
|
||||
255→ None::<&str>,
|
||||
256→ ServiceManagerAccess::CONNECT,
|
||||
257→ ) {
|
||||
258→ Ok(m) => m,
|
||||
259→ Err(_) => return Ok(()), // Can't connect, skip legacy cleanup
|
||||
260→ };
|
||||
261→
|
||||
262→ for legacy_name in LEGACY_SERVICE_NAMES {
|
||||
263→ if let Ok(service) = manager.open_service(
|
||||
264→ *legacy_name,
|
||||
265→ ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE,
|
||||
266→ ) {
|
||||
267→ info!("Found legacy service '{}', removing...", legacy_name);
|
||||
268→
|
||||
269→ // Stop if running
|
||||
270→ if let Ok(status) = service.query_status() {
|
||||
271→ if status.current_state != ServiceState::Stopped {
|
||||
272→ info!("Stopping legacy service...");
|
||||
273→ let _ = service.stop();
|
||||
274→ std::thread::sleep(Duration::from_secs(3));
|
||||
275→ }
|
||||
276→ }
|
||||
277→
|
||||
278→ // Delete the service
|
||||
279→ match service.delete() {
|
||||
280→ Ok(_) => {
|
||||
281→ println!("** Removed legacy service: {}", legacy_name);
|
||||
282→ }
|
||||
283→ Err(e) => {
|
||||
284→ warn!("Failed to delete legacy service '{}': {}", legacy_name, e);
|
||||
285→ }
|
||||
286→ }
|
||||
287→ }
|
||||
288→ }
|
||||
289→
|
||||
290→ // Also check for NSSM in registry/service config
|
||||
291→ // NSSM services have specific registry keys under HKLM\SYSTEM\CurrentControlSet\Services\{name}\Parameters
|
||||
292→ for legacy_name in LEGACY_SERVICE_NAMES {
|
||||
293→ let params_key = format!(
|
||||
294→ r"SYSTEM\CurrentControlSet\Services\{}\Parameters",
|
||||
295→ legacy_name
|
||||
296→ );
|
||||
297→ // If this key exists, it was likely an NSSM service
|
||||
298→ if let Ok(output) = std::process::Command::new("reg")
|
||||
299→ .args(["query", &format!(r"HKLM\{}", params_key)])
|
||||
300→ .output()
|
||||
301→ {
|
||||
302→ if output.status.success() {
|
||||
303→ info!("Found NSSM registry keys for '{}', cleaning up...", legacy_name);
|
||||
304→ let _ = std::process::Command::new("reg")
|
||||
305→ .args(["delete", &format!(r"HKLM\{}", params_key), "/f"])
|
||||
306→ .output();
|
||||
307→ }
|
||||
308→ }
|
||||
309→ }
|
||||
310→
|
||||
311→ Ok(())
|
||||
312→ }
|
||||
313→
|
||||
314→ /// Install the agent as a Windows service using native APIs
|
||||
315→ pub fn install(
|
||||
316→ server_url: Option<String>,
|
||||
317→ api_key: Option<String>,
|
||||
318→ skip_legacy_check: bool,
|
||||
319→ ) -> Result<()> {
|
||||
320→ info!("Installing GuruRMM Agent as Windows service...");
|
||||
321→
|
||||
322→ // Clean up legacy installations unless skipped
|
||||
323→ if !skip_legacy_check {
|
||||
324→ info!("Checking for legacy service installations...");
|
||||
325→ if let Err(e) = cleanup_legacy_services() {
|
||||
326→ warn!("Legacy cleanup warning: {}", e);
|
||||
327→ }
|
||||
328→ }
|
||||
329→
|
||||
330→ // Get the current executable path
|
||||
331→ let current_exe =
|
||||
332→ std::env::current_exe().context("Failed to get current executable path")?;
|
||||
333→
|
||||
334→ let binary_dest = PathBuf::from(format!(r"{}\gururmm-agent.exe", INSTALL_DIR));
|
||||
335→ let config_dest = PathBuf::from(format!(r"{}\agent.toml", CONFIG_DIR));
|
||||
336→
|
||||
337→ // Create directories
|
||||
338→ info!("Creating directories...");
|
||||
339→ std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?;
|
||||
340→ std::fs::create_dir_all(CONFIG_DIR).context("Failed to create config directory")?;
|
||||
341→
|
||||
342→ // Copy binary
|
||||
343→ info!("Copying binary to: {:?}", binary_dest);
|
||||
344→ std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?;
|
||||
345→
|
||||
346→ // Handle configuration
|
||||
347→ let config_needs_manual_edit;
|
||||
348→ if !config_dest.exists() {
|
||||
349→ info!("Creating config: {:?}", config_dest);
|
||||
350→
|
||||
351→ // Start with sample config
|
||||
352→ let mut config = crate::config::AgentConfig::sample();
|
||||
353→
|
||||
354→ // Apply provided values
|
||||
355→ if let Some(url) = &server_url {
|
||||
356→ config.server.url = url.clone();
|
||||
357→ }
|
||||
358→ if let Some(key) = &api_key {
|
||||
359→ config.server.api_key = key.clone();
|
||||
360→ }
|
||||
361→
|
||||
362→ let toml_str = toml::to_string_pretty(&config)?;
|
||||
363→ std::fs::write(&config_dest, toml_str).context("Failed to write config file")?;
|
||||
364→
|
||||
365→ config_needs_manual_edit = server_url.is_none() || api_key.is_none();
|
||||
366→ } else {
|
||||
367→ info!("Config already exists: {:?}", config_dest);
|
||||
368→ config_needs_manual_edit = false;
|
||||
369→
|
||||
370→ // If server_url or api_key provided, update existing config
|
||||
371→ if server_url.is_some() || api_key.is_some() {
|
||||
372→ info!("Updating existing configuration...");
|
||||
373→ let config_content = std::fs::read_to_string(&config_dest)?;
|
||||
374→ let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
|
||||
375→ .context("Failed to parse existing config")?;
|
||||
376→
|
||||
377→ if let Some(url) = &server_url {
|
||||
378→ config.server.url = url.clone();
|
||||
379→ }
|
||||
380→ if let Some(key) = &api_key {
|
||||
381→ config.server.api_key = key.clone();
|
||||
382→ }
|
||||
383→
|
||||
384→ let toml_str = toml::to_string_pretty(&config)?;
|
||||
385→ std::fs::write(&config_dest, toml_str)
|
||||
386→ .context("Failed to update config file")?;
|
||||
387→ }
|
||||
388→ }
|
||||
389→
|
||||
390→ // Open the service manager
|
||||
391→ let manager = ServiceManager::local_computer(
|
||||
392→ None::<&str>,
|
||||
393→ ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
|
||||
394→ )
|
||||
395→ .context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
396→
|
||||
397→ // Check if service already exists
|
||||
398→ if let Ok(service) = manager.open_service(
|
||||
399→ SERVICE_NAME,
|
||||
400→ ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP,
|
||||
401→ ) {
|
||||
402→ info!("Removing existing service...");
|
||||
403→
|
||||
404→ // Stop the service if running
|
||||
405→ if let Ok(status) = service.query_status() {
|
||||
406→ if status.current_state != ServiceState::Stopped {
|
||||
407→ let _ = service.stop();
|
||||
408→ std::thread::sleep(Duration::from_secs(2));
|
||||
409→ }
|
||||
410→ }
|
||||
411→
|
||||
412→ // Delete the service
|
||||
413→ service.delete().context("Failed to delete existing service")?;
|
||||
414→ drop(service);
|
||||
415→
|
||||
416→ // Wait for deletion to complete
|
||||
417→ std::thread::sleep(Duration::from_secs(2));
|
||||
418→ }
|
||||
419→
|
||||
420→ // Create the service
|
||||
421→ // The service binary is called with "service" subcommand when started by SCM
|
||||
422→ let service_binary_path = format!(r#""{}" service"#, binary_dest.display());
|
||||
423→
|
||||
424→ info!("Creating service with path: {}", service_binary_path);
|
||||
425→
|
||||
426→ let service_info = ServiceInfo {
|
||||
427→ name: OsString::from(SERVICE_NAME),
|
||||
428→ display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
||||
429→ service_type: ServiceType::OWN_PROCESS,
|
||||
430→ start_type: ServiceStartType::AutoStart,
|
||||
431→ error_control: ServiceErrorControl::Normal,
|
||||
432→ executable_path: binary_dest.clone(),
|
||||
433→ launch_arguments: vec![OsString::from("service")],
|
||||
434→ dependencies: vec![],
|
||||
435→ account_name: None, // LocalSystem
|
||||
436→ account_password: None,
|
||||
437→ };
|
||||
438→
|
||||
439→ let service = manager
|
||||
440→ .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)
|
||||
441→ .context("Failed to create service")?;
|
||||
442→
|
||||
443→ // Set description
|
||||
444→ service
|
||||
445→ .set_description(SERVICE_DESCRIPTION)
|
||||
446→ .context("Failed to set service description")?;
|
||||
447→
|
||||
448→ // Configure recovery options using sc.exe (windows-service crate doesn't support this directly)
|
||||
449→ info!("Configuring recovery options...");
|
||||
450→ let _ = std::process::Command::new("sc")
|
||||
451→ .args([
|
||||
452→ "failure",
|
||||
453→ SERVICE_NAME,
|
||||
454→ "reset=86400",
|
||||
455→ "actions=restart/60000/restart/60000/restart/60000",
|
||||
456→ ])
|
||||
457→ .output();
|
||||
458→
|
||||
459→ println!("\n** GuruRMM Agent installed successfully!");
|
||||
460→ println!("\nInstalled files:");
|
||||
461→ println!(" Binary: {:?}", binary_dest);
|
||||
462→ println!(" Config: {:?}", config_dest);
|
||||
463→
|
||||
464→ if config_needs_manual_edit {
|
||||
465→ println!("\n** IMPORTANT: Edit {:?} with your server URL and API key!", config_dest);
|
||||
466→ println!("\nNext steps:");
|
||||
467→ println!(" 1. Edit {:?} with your server URL and API key", config_dest);
|
||||
468→ println!(" 2. Start the service:");
|
||||
469→ println!(" gururmm-agent start");
|
||||
470→ println!(" Or: sc start {}", SERVICE_NAME);
|
||||
471→ } else {
|
||||
472→ println!("\nStarting service...");
|
||||
473→ if let Err(e) = start() {
|
||||
474→ println!("** Failed to start service: {}. Start manually with:", e);
|
||||
475→ println!(" gururmm-agent start");
|
||||
476→ } else {
|
||||
477→ println!("** Service started successfully!");
|
||||
478→ }
|
||||
479→ }
|
||||
480→
|
||||
481→ println!("\nUseful commands:");
|
||||
482→ println!(" Status: gururmm-agent status");
|
||||
483→ println!(" Stop: gururmm-agent stop");
|
||||
484→ println!(" Start: gururmm-agent start");
|
||||
485→
|
||||
486→ Ok(())
|
||||
487→ }
|
||||
488→
|
||||
489→ /// Uninstall the Windows service
|
||||
490→ pub fn uninstall() -> Result<()> {
|
||||
491→ info!("Uninstalling GuruRMM Agent...");
|
||||
492→
|
||||
493→ let binary_path = PathBuf::from(format!(r"{}\gururmm-agent.exe", INSTALL_DIR));
|
||||
494→
|
||||
495→ // Open the service manager
|
||||
496→ let manager = ServiceManager::local_computer(
|
||||
497→ None::<&str>,
|
||||
498→ ServiceManagerAccess::CONNECT,
|
||||
499→ )
|
||||
500→ .context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
501→
|
||||
502→ // Open the service
|
||||
503→ match manager.open_service(
|
||||
504→ SERVICE_NAME,
|
||||
505→ ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE,
|
||||
506→ ) {
|
||||
507→ Ok(service) => {
|
||||
508→ // Stop if running
|
||||
509→ if let Ok(status) = service.query_status() {
|
||||
510→ if status.current_state != ServiceState::Stopped {
|
||||
511→ info!("Stopping service...");
|
||||
512→ let _ = service.stop();
|
||||
513→ std::thread::sleep(Duration::from_secs(3));
|
||||
514→ }
|
||||
515→ }
|
||||
516→
|
||||
517→ // Delete the service
|
||||
518→ info!("Deleting service...");
|
||||
519→ service.delete().context("Failed to delete service")?;
|
||||
520→ }
|
||||
521→ Err(_) => {
|
||||
522→ warn!("Service was not installed");
|
||||
523→ }
|
||||
524→ }
|
||||
525→
|
||||
526→ // Remove binary
|
||||
527→ if binary_path.exists() {
|
||||
528→ info!("Removing binary: {:?}", binary_path);
|
||||
529→ // Wait a bit for service to fully stop
|
||||
530→ std::thread::sleep(Duration::from_secs(1));
|
||||
531→ if let Err(e) = std::fs::remove_file(&binary_path) {
|
||||
532→ warn!("Failed to remove binary (may be in use): {}", e);
|
||||
533→ }
|
||||
534→ }
|
||||
535→
|
||||
536→ // Remove install directory if empty
|
||||
537→ let _ = std::fs::remove_dir(INSTALL_DIR);
|
||||
538→
|
||||
539→ println!("\n** GuruRMM Agent uninstalled successfully!");
|
||||
540→ println!(
|
||||
541→ "\nNote: Config directory {:?} was preserved.",
|
||||
542→ CONFIG_DIR
|
||||
543→ );
|
||||
544→ println!("Remove it manually if no longer needed.");
|
||||
545→
|
||||
546→ Ok(())
|
||||
547→ }
|
||||
548→
|
||||
549→ /// Start the installed service
|
||||
550→ pub fn start() -> Result<()> {
|
||||
551→ info!("Starting GuruRMM Agent service...");
|
||||
552→
|
||||
553→ let manager = ServiceManager::local_computer(
|
||||
554→ None::<&str>,
|
||||
555→ ServiceManagerAccess::CONNECT,
|
||||
556→ )
|
||||
557→ .context("Failed to connect to Service Control Manager")?;
|
||||
558→
|
||||
559→ let service = manager
|
||||
560→ .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS)
|
||||
561→ .context("Failed to open service. Is it installed?")?;
|
||||
562→
|
||||
563→ service
|
||||
564→ .start::<String>(&[])
|
||||
565→ .context("Failed to start service")?;
|
||||
566→
|
||||
567→ // Wait briefly and check status
|
||||
568→ std::thread::sleep(Duration::from_secs(2));
|
||||
569→
|
||||
570→ let status = service.query_status()?;
|
||||
571→ match status.current_state {
|
||||
572→ ServiceState::Running => {
|
||||
573→ println!("** Service started successfully");
|
||||
574→ println!("Check status: gururmm-agent status");
|
||||
575→ }
|
||||
576→ ServiceState::StartPending => {
|
||||
577→ println!("** Service is starting...");
|
||||
578→ println!("Check status: gururmm-agent status");
|
||||
579→ }
|
||||
580→ other => {
|
||||
581→ println!("Service state: {:?}", other);
|
||||
582→ }
|
||||
583→ }
|
||||
584→
|
||||
585→ Ok(())
|
||||
586→ }
|
||||
587→
|
||||
588→ /// Stop the installed service
|
||||
589→ pub fn stop() -> Result<()> {
|
||||
590→ info!("Stopping GuruRMM Agent service...");
|
||||
591→
|
||||
592→ let manager = ServiceManager::local_computer(
|
||||
593→ None::<&str>,
|
||||
594→ ServiceManagerAccess::CONNECT,
|
||||
595→ )
|
||||
596→ .context("Failed to connect to Service Control Manager")?;
|
||||
597→
|
||||
598→ let service = manager
|
||||
599→ .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS)
|
||||
600→ .context("Failed to open service. Is it installed?")?;
|
||||
601→
|
||||
602→ service.stop().context("Failed to stop service")?;
|
||||
603→
|
||||
604→ // Wait and verify
|
||||
605→ std::thread::sleep(Duration::from_secs(2));
|
||||
606→
|
||||
607→ let status = service.query_status()?;
|
||||
608→ match status.current_state {
|
||||
609→ ServiceState::Stopped => {
|
||||
610→ println!("** Service stopped successfully");
|
||||
611→ }
|
||||
612→ ServiceState::StopPending => {
|
||||
613→ println!("** Service is stopping...");
|
||||
614→ }
|
||||
615→ other => {
|
||||
616→ println!("Service state: {:?}", other);
|
||||
617→ }
|
||||
618→ }
|
||||
619→
|
||||
620→ Ok(())
|
||||
621→ }
|
||||
622→
|
||||
623→ /// Query service status
|
||||
624→ pub fn status() -> Result<()> {
|
||||
625→ let manager = ServiceManager::local_computer(
|
||||
626→ None::<&str>,
|
||||
627→ ServiceManagerAccess::CONNECT,
|
||||
628→ )
|
||||
629→ .context("Failed to connect to Service Control Manager")?;
|
||||
630→
|
||||
631→ match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) {
|
||||
632→ Ok(service) => {
|
||||
633→ let status = service.query_status()?;
|
||||
634→ println!("GuruRMM Agent Service Status");
|
||||
635→ println!("============================");
|
||||
636→ println!("Service Name: {}", SERVICE_NAME);
|
||||
637→ println!("Display Name: {}", SERVICE_DISPLAY_NAME);
|
||||
638→ println!("State: {:?}", status.current_state);
|
||||
639→ println!(
|
||||
640→ "Binary: {}\\gururmm-agent.exe",
|
||||
641→ INSTALL_DIR
|
||||
642→ );
|
||||
643→ println!("Config: {}\\agent.toml", CONFIG_DIR);
|
||||
644→ }
|
||||
645→ Err(_) => {
|
||||
646→ println!("GuruRMM Agent Service Status");
|
||||
647→ println!("============================");
|
||||
648→ println!("Status: NOT INSTALLED");
|
||||
649→ println!("\nTo install: gururmm-agent install");
|
||||
650→ }
|
||||
651→ }
|
||||
652→
|
||||
653→ Ok(())
|
||||
654→ }
|
||||
655→}
|
||||
656→
|
||||
|
||||
<system-reminder>
|
||||
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.
|
||||
</system-reminder>
|
||||
@@ -0,0 +1 @@
|
||||
File has been unexpectedly modified. Read it again before attempting to write it.
|
||||
Reference in New Issue
Block a user