Files
claudetools/imported-conversations/general-work/claude-projects/99918cbf-1f1c-4e49-ace0-f7a73ae40c80/tool-results/toolu_01VpHAurcyCCLBa6rabFSxXK.txt
Mike Swanson 75ce1c2fd5 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>
2026-01-17 16:23:52 -07:00

773 lines
30 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ipc;
9→mod metrics;
10→mod service;
11→mod transport;
12→mod updater;
13→
14→use anyhow::{Context, Result};
15→use clap::{Parser, Subcommand};
16→use std::path::PathBuf;
17→use std::sync::Arc;
18→use tokio::sync::RwLock;
19→use tracing::{error, info, warn};
20→
21→use crate::config::AgentConfig;
22→use crate::metrics::MetricsCollector;
23→use crate::transport::WebSocketClient;
24→
25→/// GuruRMM Agent - Remote Monitoring and Management
26→#[derive(Parser)]
27→#[command(name = "gururmm-agent")]
28→#[command(author, version, about, long_about = None)]
29→struct Cli {
30→ /// Path to configuration file
31→ #[arg(short, long, default_value = "agent.toml")]
32→ config: PathBuf,
33→
34→ /// Subcommand to run
35→ #[command(subcommand)]
36→ command: Option<Commands>,
37→}
38→
39→#[derive(Subcommand)]
40→enum Commands {
41→ /// Run the agent (default)
42→ Run,
43→
44→ /// Install as a system service
45→ Install {
46→ /// Server WebSocket URL (e.g., wss://rmm-api.example.com/ws)
47→ #[arg(long)]
48→ server_url: Option<String>,
49→
50→ /// API key for authentication
51→ #[arg(long)]
52→ api_key: Option<String>,
53→
54→ /// Skip legacy service detection and cleanup
55→ #[arg(long, default_value = "false")]
56→ skip_legacy_check: bool,
57→ },
58→
59→ /// Uninstall the system service
60→ Uninstall,
61→
62→ /// Start the installed service
63→ Start,
64→
65→ /// Stop the installed service
66→ Stop,
67→
68→ /// Show agent status
69→ Status,
70→
71→ /// Generate a sample configuration file
72→ GenerateConfig {
73→ /// Output path for config file
74→ #[arg(short, long, default_value = "agent.toml")]
75→ output: PathBuf,
76→ },
77→
78→ /// Run as Windows service (called by SCM, not for manual use)
79→ #[command(hide = true)]
80→ Service,
81→}
82→
83→/// Shared application state
84→pub struct AppState {
85→ pub config: AgentConfig,
86→ pub metrics_collector: MetricsCollector,
87→ pub connected: RwLock<bool>,
88→ pub last_checkin: RwLock<Option<chrono::DateTime<chrono::Utc>>>,
89→ pub tray_policy: RwLock<ipc::TrayPolicy>,
90→}
91→
92→#[tokio::main]
93→async fn main() -> Result<()> {
94→ // Initialize logging
95→ tracing_subscriber::fmt()
96→ .with_env_filter(
97→ tracing_subscriber::EnvFilter::from_default_env()
98→ .add_directive("gururmm_agent=info".parse()?)
99→ .add_directive("info".parse()?),
100→ )
101→ .init();
102→
103→ let cli = Cli::parse();
104→
105→ match cli.command.unwrap_or(Commands::Run) {
106→ Commands::Run => run_agent(cli.config).await,
107→ Commands::Install { server_url, api_key, skip_legacy_check } => {
108→ install_service(server_url, api_key, skip_legacy_check).await
109→ }
110→ Commands::Uninstall => uninstall_service().await,
111→ Commands::Start => start_service().await,
112→ Commands::Stop => stop_service().await,
113→ Commands::Status => show_status(cli.config).await,
114→ Commands::GenerateConfig { output } => generate_config(output).await,
115→ Commands::Service => run_as_windows_service(),
116→ }
117→}
118→
119→/// Run as a Windows service (called by SCM)
120→fn run_as_windows_service() -> Result<()> {
121→ #[cfg(windows)]
122→ {
123→ service::windows::run_as_service()
124→ }
125→
126→ #[cfg(not(windows))]
127→ {
128→ anyhow::bail!("Windows service mode is only available on Windows");
129→ }
130→}
131→
132→/// Main agent runtime loop
133→async fn run_agent(config_path: PathBuf) -> Result<()> {
134→ info!("GuruRMM Agent starting...");
135→
136→ // Load configuration
137→ let config = AgentConfig::load(&config_path)?;
138→ info!("Loaded configuration from {:?}", config_path);
139→ info!("Server URL: {}", config.server.url);
140→
141→ // Initialize metrics collector
142→ let metrics_collector = MetricsCollector::new();
143→ info!("Metrics collector initialized");
144→
145→ // Create channels for IPC commands
146→ let (force_checkin_tx, mut force_checkin_rx) = tokio::sync::mpsc::channel::<()>(8);
147→ let (stop_agent_tx, mut stop_agent_rx) = tokio::sync::mpsc::channel::<()>(1);
148→
149→ // Create shared state
150→ let state = Arc::new(AppState {
151→ config: config.clone(),
152→ metrics_collector,
153→ connected: RwLock::new(false),
154→ last_checkin: RwLock::new(None),
155→ tray_policy: RwLock::new(ipc::TrayPolicy::default_permissive()),
156→ });
157→
158→ // Create IPC state for tray communication
159→ let device_id = device_id::get_device_id();
160→ let hostname = hostname::get()
161→ .map(|h| h.to_string_lossy().to_string())
162→ .unwrap_or_else(|_| "unknown".to_string());
163→
164→ let initial_status = ipc::AgentStatus {
165→ connected: false,
166→ last_checkin: None,
167→ server_url: config.server.url.clone(),
168→ agent_version: env!("CARGO_PKG_VERSION").to_string(),
169→ device_id,
170→ hostname,
171→ error: None,
172→ };
173→
174→ let ipc_state = Arc::new(ipc::IpcState::new(
175→ initial_status,
176→ ipc::TrayPolicy::default_permissive(),
177→ force_checkin_tx,
178→ stop_agent_tx,
179→ ));
180→
181→ // Start IPC server for tray application
182→ let ipc_server_state = Arc::clone(&ipc_state);
183→ let ipc_handle = tokio::spawn(async move {
184→ if let Err(e) = ipc::server::run_ipc_server(ipc_server_state).await {
185→ error!("IPC server error: {}", e);
186→ }
187→ });
188→ info!("IPC server started");
189→
190→ // Start the WebSocket client with auto-reconnect
191→ let ws_state = Arc::clone(&state);
192→ let ws_handle = tokio::spawn(async move {
193→ loop {
194→ info!("Connecting to server...");
195→ match WebSocketClient::connect_and_run(Arc::clone(&ws_state)).await {
196→ Ok(_) => {
197→ warn!("WebSocket connection closed normally, reconnecting...");
198→ }
199→ Err(e) => {
200→ error!("WebSocket error: {}, reconnecting in 10 seconds...", e);
201→ }
202→ }
203→
204→ // Mark as disconnected
205→ *ws_state.connected.write().await = false;
206→
207→ // Wait before reconnecting
208→ tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
209→ }
210→ });
211→
212→ // Start metrics collection loop
213→ let metrics_state = Arc::clone(&state);
214→ let metrics_handle = tokio::spawn(async move {
215→ let interval = metrics_state.config.metrics.interval_seconds;
216→ let mut interval_timer = tokio::time::interval(tokio::time::Duration::from_secs(interval));
217→
218→ loop {
219→ interval_timer.tick().await;
220→
221→ // Collect metrics (they'll be sent via WebSocket if connected)
222→ let metrics = metrics_state.metrics_collector.collect().await;
223→ if *metrics_state.connected.read().await {
224→ info!(
225→ "Metrics: CPU={:.1}%, Mem={:.1}%, Disk={:.1}%",
226→ metrics.cpu_percent, metrics.memory_percent, metrics.disk_percent
227→ );
228→ }
229→ }
230→ });
231→
232→ // Task to update IPC status periodically
233→ let ipc_update_state = Arc::clone(&state);
234→ let ipc_update_ipc = Arc::clone(&ipc_state);
235→ let ipc_update_handle = tokio::spawn(async move {
236→ let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
237→ loop {
238→ interval.tick().await;
239→
240→ let connected = *ipc_update_state.connected.read().await;
241→ let last_checkin = ipc_update_state.last_checkin.read().await.map(|dt| dt.to_rfc3339());
242→
243→ let status = ipc::AgentStatus {
244→ connected,
245→ last_checkin,
246→ server_url: ipc_update_state.config.server.url.clone(),
247→ agent_version: env!("CARGO_PKG_VERSION").to_string(),
248→ device_id: device_id::get_device_id(),
249→ hostname: hostname::get()
250→ .map(|h| h.to_string_lossy().to_string())
251→ .unwrap_or_else(|_| "unknown".to_string()),
252→ error: if connected { None } else { Some("Disconnected".to_string()) },
253→ };
254→
255→ ipc_update_ipc.update_status(status).await;
256→ }
257→ });
258→
259→ // Wait for shutdown signal or stop request
260→ tokio::select! {
261→ _ = tokio::signal::ctrl_c() => {
262→ info!("Received shutdown signal");
263→ }
264→ _ = stop_agent_rx.recv() => {
265→ info!("Received stop request from tray");
266→ }
267→ _ = ws_handle => {
268→ error!("WebSocket task ended unexpectedly");
269→ }
270→ _ = metrics_handle => {
271→ error!("Metrics task ended unexpectedly");
272→ }
273→ _ = ipc_handle => {
274→ error!("IPC server ended unexpectedly");
275→ }
276→ _ = ipc_update_handle => {
277→ error!("IPC update task ended unexpectedly");
278→ }
279→ }
280→
281→ info!("GuruRMM Agent shutting down");
282→ Ok(())
283→}
284→
285→/// Install the agent as a system service
286→async fn install_service(
287→ server_url: Option<String>,
288→ api_key: Option<String>,
289→ skip_legacy_check: bool,
290→) -> Result<()> {
291→ #[cfg(windows)]
292→ {
293→ service::windows::install(server_url, api_key, skip_legacy_check)
294→ }
295→
296→ #[cfg(target_os = "linux")]
297→ {
298→ install_systemd_service(server_url, api_key, skip_legacy_check).await
299→ }
300→
301→ #[cfg(target_os = "macos")]
302→ {
303→ let _ = (server_url, api_key, skip_legacy_check); // Suppress unused warnings
304→ info!("Installing GuruRMM Agent as launchd service...");
305→ todo!("macOS launchd service installation not yet implemented");
306→ }
307→}
308→
309→/// Legacy service names to check for and clean up (Linux)
310→#[cfg(target_os = "linux")]
311→const LINUX_LEGACY_SERVICE_NAMES: &[&str] = &[
312→ "gururmm", // Old name without -agent suffix
313→ "guru-rmm-agent", // Alternative naming
314→ "GuruRMM-Agent", // Case variant
315→];
316→
317→/// Clean up legacy Linux service installations
318→#[cfg(target_os = "linux")]
319→fn cleanup_legacy_linux_services() -> Result<()> {
320→ use std::process::Command;
321→
322→ info!("Checking for legacy service installations...");
323→
324→ for legacy_name in LINUX_LEGACY_SERVICE_NAMES {
325→ // Check if service exists
326→ let status = Command::new("systemctl")
327→ .args(["status", legacy_name])
328→ .output();
329→
330→ if let Ok(output) = status {
331→ if output.status.success() || String::from_utf8_lossy(&output.stderr).contains("Loaded:") {
332→ info!("Found legacy service '{}', removing...", legacy_name);
333→
334→ // Stop the service
335→ let _ = Command::new("systemctl")
336→ .args(["stop", legacy_name])
337→ .status();
338→
339→ // Disable the service
340→ let _ = Command::new("systemctl")
341→ .args(["disable", legacy_name])
342→ .status();
343→
344→ // Remove unit file
345→ let unit_file = format!("/etc/systemd/system/{}.service", legacy_name);
346→ if std::path::Path::new(&unit_file).exists() {
347→ info!("Removing legacy unit file: {}", unit_file);
348→ let _ = std::fs::remove_file(&unit_file);
349→ }
350→ }
351→ }
352→ }
353→
354→ // Check for legacy binaries in common locations
355→ let legacy_binary_locations = [
356→ "/usr/local/bin/gururmm",
357→ "/usr/bin/gururmm",
358→ "/opt/gururmm/gururmm",
359→ "/opt/gururmm/agent",
360→ ];
361→
362→ for legacy_path in legacy_binary_locations {
363→ if std::path::Path::new(legacy_path).exists() {
364→ info!("Found legacy binary at '{}', removing...", legacy_path);
365→ let _ = std::fs::remove_file(legacy_path);
366→ }
367→ }
368→
369→ // Reload systemd to pick up removed unit files
370→ let _ = Command::new("systemctl")
371→ .args(["daemon-reload"])
372→ .status();
373→
374→ Ok(())
375→}
376→
377→/// Install as a systemd service (Linux)
378→#[cfg(target_os = "linux")]
379→async fn install_systemd_service(
380→ server_url: Option<String>,
381→ api_key: Option<String>,
382→ skip_legacy_check: bool,
383→) -> Result<()> {
384→ use std::process::Command;
385→
386→ const SERVICE_NAME: &str = "gururmm-agent";
387→ const INSTALL_DIR: &str = "/usr/local/bin";
388→ const CONFIG_DIR: &str = "/etc/gururmm";
389→ const SYSTEMD_DIR: &str = "/etc/systemd/system";
390→
391→ info!("Installing GuruRMM Agent as systemd service...");
392→
393→ // Check if running as root
394→ if !nix::unistd::geteuid().is_root() {
395→ anyhow::bail!("Installation requires root privileges. Please run with sudo.");
396→ }
397→
398→ // Clean up legacy installations unless skipped
399→ if !skip_legacy_check {
400→ if let Err(e) = cleanup_legacy_linux_services() {
401→ warn!("Legacy cleanup warning: {}", e);
402→ }
403→ }
404→
405→ // Get the current executable path
406→ let current_exe = std::env::current_exe()
407→ .context("Failed to get current executable path")?;
408→
409→ let binary_dest = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
410→ let config_dest = format!("{}/agent.toml", CONFIG_DIR);
411→ let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);
412→
413→ // Create config directory
414→ info!("Creating config directory: {}", CONFIG_DIR);
415→ std::fs::create_dir_all(CONFIG_DIR)
416→ .context("Failed to create config directory")?;
417→
418→ // Copy binary
419→ info!("Copying binary to: {}", binary_dest);
420→ std::fs::copy(&current_exe, &binary_dest)
421→ .context("Failed to copy binary")?;
422→
423→ // Make binary executable
424→ Command::new("chmod")
425→ .args(["+x", &binary_dest])
426→ .status()
427→ .context("Failed to set binary permissions")?;
428→
429→ // Handle configuration
430→ let config_needs_manual_edit;
431→ if !std::path::Path::new(&config_dest).exists() {
432→ info!("Creating config: {}", config_dest);
433→
434→ // Start with sample config
435→ let mut config = crate::config::AgentConfig::sample();
436→
437→ // Apply provided values
438→ if let Some(url) = &server_url {
439→ config.server.url = url.clone();
440→ }
441→ if let Some(key) = &api_key {
442→ config.server.api_key = key.clone();
443→ }
444→
445→ let toml_str = toml::to_string_pretty(&config)?;
446→ std::fs::write(&config_dest, toml_str)
447→ .context("Failed to write config file")?;
448→
449→ // Set restrictive permissions on config (contains API key)
450→ Command::new("chmod")
451→ .args(["600", &config_dest])
452→ .status()
453→ .context("Failed to set config permissions")?;
454→
455→ config_needs_manual_edit = server_url.is_none() || api_key.is_none();
456→ } else {
457→ info!("Config already exists: {}", config_dest);
458→ config_needs_manual_edit = false;
459→
460→ // If server_url or api_key provided, update existing config
461→ if server_url.is_some() || api_key.is_some() {
462→ info!("Updating existing configuration...");
463→ let config_content = std::fs::read_to_string(&config_dest)?;
464→ let mut config: crate::config::AgentConfig = toml::from_str(&config_content)
465→ .context("Failed to parse existing config")?;
466→
467→ if let Some(url) = &server_url {
468→ config.server.url = url.clone();
469→ }
470→ if let Some(key) = &api_key {
471→ config.server.api_key = key.clone();
472→ }
473→
474→ let toml_str = toml::to_string_pretty(&config)?;
475→ std::fs::write(&config_dest, toml_str)
476→ .context("Failed to update config file")?;
477→ }
478→ }
479→
480→ // Create systemd unit file
481→ let unit_content = format!(r#"[Unit]
482→Description=GuruRMM Agent - Remote Monitoring and Management
483→Documentation=https://github.com/azcomputerguru/gururmm
484→After=network-online.target
485→Wants=network-online.target
486→
487→[Service]
488→Type=simple
489→ExecStart={binary} --config {config} run
490→Restart=always
491→RestartSec=10
492→StandardOutput=journal
493→StandardError=journal
494→SyslogIdentifier={service}
495→
496→# Security hardening
497→NoNewPrivileges=true
498→ProtectSystem=strict
499→ProtectHome=read-only
500→PrivateTmp=true
501→ReadWritePaths=/var/log
502→
503→[Install]
504→WantedBy=multi-user.target
505→"#,
506→ binary = binary_dest,
507→ config = config_dest,
508→ service = SERVICE_NAME
509→ );
510→
511→ info!("Creating systemd unit file: {}", unit_file);
512→ std::fs::write(&unit_file, unit_content)
513→ .context("Failed to write systemd unit file")?;
514→
515→ // Reload systemd daemon
516→ info!("Reloading systemd daemon...");
517→ let status = Command::new("systemctl")
518→ .args(["daemon-reload"])
519→ .status()
520→ .context("Failed to reload systemd")?;
521→
522→ if !status.success() {
523→ anyhow::bail!("systemctl daemon-reload failed");
524→ }
525→
526→ // Enable the service
527→ info!("Enabling service...");
528→ let status = Command::new("systemctl")
529→ .args(["enable", SERVICE_NAME])
530→ .status()
531→ .context("Failed to enable service")?;
532→
533→ if !status.success() {
534→ anyhow::bail!("systemctl enable failed");
535→ }
536→
537→ println!("\n✓ GuruRMM Agent installed successfully!");
538→ println!("\nInstalled files:");
539→ println!(" Binary: {}", binary_dest);
540→ println!(" Config: {}", config_dest);
541→ println!(" Service: {}", unit_file);
542→
543→ if config_needs_manual_edit {
544→ println!("\n⚠ IMPORTANT: Edit {} with your server URL and API key!", config_dest);
545→ println!("\nNext steps:");
546→ println!(" 1. Edit {} with your server URL and API key", config_dest);
547→ println!(" 2. Start the service: sudo systemctl start {}", SERVICE_NAME);
548→ } else {
549→ println!("\nStarting service...");
550→ let status = Command::new("systemctl")
551→ .args(["start", SERVICE_NAME])
552→ .status();
553→
554→ if status.is_ok() && status.unwrap().success() {
555→ println!("✓ Service started successfully!");
556→ } else {
557→ println!("⚠️ Failed to start service. Check logs: sudo journalctl -u {} -f", SERVICE_NAME);
558→ }
559→ }
560→
561→ println!("\nUseful commands:");
562→ println!(" Status: sudo systemctl status {}", SERVICE_NAME);
563→ println!(" Logs: sudo journalctl -u {} -f", SERVICE_NAME);
564→ println!(" Stop: sudo systemctl stop {}", SERVICE_NAME);
565→ println!(" Start: sudo systemctl start {}", SERVICE_NAME);
566→
567→ Ok(())
568→}
569→
570→/// Uninstall the system service
571→async fn uninstall_service() -> Result<()> {
572→ #[cfg(windows)]
573→ {
574→ service::windows::uninstall()
575→ }
576→
577→ #[cfg(target_os = "linux")]
578→ {
579→ uninstall_systemd_service().await
580→ }
581→
582→ #[cfg(target_os = "macos")]
583→ {
584→ todo!("macOS service uninstallation not yet implemented");
585→ }
586→}
587→
588→/// Uninstall systemd service (Linux)
589→#[cfg(target_os = "linux")]
590→async fn uninstall_systemd_service() -> Result<()> {
591→ use std::process::Command;
592→
593→ const SERVICE_NAME: &str = "gururmm-agent";
594→ const INSTALL_DIR: &str = "/usr/local/bin";
595→ const CONFIG_DIR: &str = "/etc/gururmm";
596→ const SYSTEMD_DIR: &str = "/etc/systemd/system";
597→
598→ info!("Uninstalling GuruRMM Agent...");
599→
600→ if !nix::unistd::geteuid().is_root() {
601→ anyhow::bail!("Uninstallation requires root privileges. Please run with sudo.");
602→ }
603→
604→ let binary_path = format!("{}/{}", INSTALL_DIR, SERVICE_NAME);
605→ let unit_file = format!("{}/{}.service", SYSTEMD_DIR, SERVICE_NAME);
606→
607→ // Stop the service if running
608→ info!("Stopping service...");
609→ let _ = Command::new("systemctl")
610→ .args(["stop", SERVICE_NAME])
611→ .status();
612→
613→ // Disable the service
614→ info!("Disabling service...");
615→ let _ = Command::new("systemctl")
616→ .args(["disable", SERVICE_NAME])
617→ .status();
618→
619→ // Remove unit file
620→ if std::path::Path::new(&unit_file).exists() {
621→ info!("Removing unit file: {}", unit_file);
622→ std::fs::remove_file(&unit_file)?;
623→ }
624→
625→ // Remove binary
626→ if std::path::Path::new(&binary_path).exists() {
627→ info!("Removing binary: {}", binary_path);
628→ std::fs::remove_file(&binary_path)?;
629→ }
630→
631→ // Reload systemd
632→ let _ = Command::new("systemctl")
633→ .args(["daemon-reload"])
634→ .status();
635→
636→ println!("\n✓ GuruRMM Agent uninstalled successfully!");
637→ println!("\nNote: Config directory {} was preserved.", CONFIG_DIR);
638→ println!("Remove it manually if no longer needed: sudo rm -rf {}", CONFIG_DIR);
639→
640→ Ok(())
641→}
642→
643→/// Start the installed service
644→async fn start_service() -> Result<()> {
645→ #[cfg(windows)]
646→ {
647→ service::windows::start()
648→ }
649→
650→ #[cfg(target_os = "linux")]
651→ {
652→ use std::process::Command;
653→
654→ info!("Starting GuruRMM Agent service...");
655→
656→ let status = Command::new("systemctl")
657→ .args(["start", "gururmm-agent"])
658→ .status()
659→ .context("Failed to start service")?;
660→
661→ if status.success() {
662→ println!("** Service started successfully");
663→ println!("Check status: sudo systemctl status gururmm-agent");
664→ } else {
665→ anyhow::bail!("Failed to start service. Check: sudo journalctl -u gururmm-agent -n 50");
666→ }
667→
668→ Ok(())
669→ }
670→
671→ #[cfg(target_os = "macos")]
672→ {
673→ todo!("macOS service start not yet implemented");
674→ }
675→}
676→
677→/// Stop the installed service
678→async fn stop_service() -> Result<()> {
679→ #[cfg(windows)]
680→ {
681→ service::windows::stop()
682→ }
683→
684→ #[cfg(target_os = "linux")]
685→ {
686→ use std::process::Command;
687→
688→ info!("Stopping GuruRMM Agent service...");
689→
690→ let status = Command::new("systemctl")
691→ .args(["stop", "gururmm-agent"])
692→ .status()
693→ .context("Failed to stop service")?;
694→
695→ if status.success() {
696→ println!("** Service stopped successfully");
697→ } else {
698→ anyhow::bail!("Failed to stop service");
699→ }
700→
701→ Ok(())
702→ }
703→
704→ #[cfg(target_os = "macos")]
705→ {
706→ todo!("macOS service stop not yet implemented");
707→ }
708→}
709→
710→/// Show agent status
711→async fn show_status(config_path: PathBuf) -> Result<()> {
712→ // On Windows, show service status
713→ #[cfg(windows)]
714→ {
715→ service::windows::status()?;
716→ println!();
717→ }
718→
719→ // Try to load config for additional info
720→ match AgentConfig::load(&config_path) {
721→ Ok(config) => {
722→ println!("Configuration");
723→ println!("=============");
724→ println!("Config file: {:?}", config_path);
725→ println!("Server URL: {}", config.server.url);
726→ println!("Metrics interval: {} seconds", config.metrics.interval_seconds);
727→ println!("Watchdog enabled: {}", config.watchdog.enabled);
728→
729→ // Collect current metrics
730→ let collector = MetricsCollector::new();
731→ let metrics = collector.collect().await;
732→
733→ println!("\nCurrent System Metrics:");
734→ println!(" CPU Usage: {:.1}%", metrics.cpu_percent);
735→ println!(" Memory Usage: {:.1}%", metrics.memory_percent);
736→ println!(
737→ " Memory Used: {:.2} GB",
738→ metrics.memory_used_bytes as f64 / 1_073_741_824.0
739→ );
740→ println!(" Disk Usage: {:.1}%", metrics.disk_percent);
741→ println!(
742→ " Disk Used: {:.2} GB",
743→ metrics.disk_used_bytes as f64 / 1_073_741_824.0
744→ );
745→ }
746→ Err(_) => {
747→ println!("\nConfig file {:?} not found or invalid.", config_path);
748→ #[cfg(windows)]
749→ println!("Service config location: {}\\agent.toml", service::windows::CONFIG_DIR);
750→ }
751→ }
752→
753→ Ok(())
754→}
755→
756→/// Generate a sample configuration file
757→async fn generate_config(output: PathBuf) -> Result<()> {
758→ let sample_config = AgentConfig::sample();
759→ let toml_str = toml::to_string_pretty(&sample_config)?;
760→
761→ std::fs::write(&output, toml_str)?;
762→ println!("Sample configuration written to {:?}", output);
763→ println!("\nEdit this file with your server URL and API key, then run:");
764→ println!(" gururmm-agent --config {:?} run", output);
765→
766→ Ok(())
767→}
768→
<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>