     1→//! Agent self-update module
     2→//!
     3→//! Handles downloading, verifying, and installing agent updates.
     4→//! Features:
     5→//! - Download new binary via HTTPS
     6→//! - SHA256 checksum verification
     7→//! - Atomic binary replacement
     8→//! - Auto-rollback if agent fails to restart
     9→
    10→use std::path::{Path, PathBuf};
    11→
    12→use anyhow::{Context, Result};
    13→use sha2::{Sha256, Digest};
    14→use tokio::fs;
    15→use tokio::io::AsyncWriteExt;
    16→use tracing::{debug, error, info, warn};
    17→use uuid::Uuid;
    18→
    19→use crate::transport::{UpdatePayload, UpdateResultPayload, UpdateStatus};
    20→
    21→/// Configuration for the updater
    22→#[derive(Debug, Clone)]
    23→pub struct UpdaterConfig {
    24→    /// Path to the current agent binary
    25→    pub binary_path: PathBuf,
    26→    /// Directory for config and backup files
    27→    pub config_dir: PathBuf,
    28→    /// Rollback timeout in seconds
    29→    pub rollback_timeout_secs: u64,
    30→}
    31→
    32→impl Default for UpdaterConfig {
    33→    fn default() -> Self {
    34→        Self {
    35→            binary_path: Self::detect_binary_path(),
    36→            config_dir: Self::detect_config_dir(),
    37→            rollback_timeout_secs: 180,
    38→        }
    39→    }
    40→}
    41→
    42→impl UpdaterConfig {
    43→    /// Detect the path to the currently running binary
    44→    fn detect_binary_path() -> PathBuf {
    45→        std::env::current_exe().unwrap_or_else(|_| {
    46→            #[cfg(windows)]
    47→            { PathBuf::from(r"C:\Program Files\GuruRMM\gururmm-agent.exe") }
    48→            #[cfg(not(windows))]
    49→            { PathBuf::from("/usr/local/bin/gururmm-agent") }
    50→        })
    51→    }
    52→
    53→    /// Detect the config directory
    54→    fn detect_config_dir() -> PathBuf {
    55→        #[cfg(windows)]
    56→        { PathBuf::from(r"C:\ProgramData\GuruRMM") }
    57→        #[cfg(not(windows))]
    58→        { PathBuf::from("/etc/gururmm") }
    59→    }
    60→
    61→    /// Get the backup binary path
    62→    pub fn backup_path(&self) -> PathBuf {
    63→        self.config_dir.join("gururmm-agent.backup")
    64→    }
    65→
    66→    /// Get the pending update info path (stores update_id for reconnection)
    67→    pub fn pending_update_path(&self) -> PathBuf {
    68→        self.config_dir.join("pending-update.json")
    69→    }
    70→}
    71→
    72→/// Pending update information (persisted to disk before restart)
    73→#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
    74→pub struct PendingUpdateInfo {
    75→    pub update_id: Uuid,
    76→    pub old_version: String,
    77→    pub target_version: String,
    78→}
    79→
    80→/// Agent updater
    81→pub struct AgentUpdater {
    82→    config: UpdaterConfig,
    83→    http_client: reqwest::Client,
    84→}
    85→
    86→impl AgentUpdater {
    87→    /// Create a new updater
    88→    pub fn new(config: UpdaterConfig) -> Self {
    89→        let http_client = reqwest::Client::builder()
    90→            .timeout(std::time::Duration::from_secs(300))
    91→            .build()
    92→            .expect("Failed to create HTTP client");
    93→
    94→        Self { config, http_client }
    95→    }
    96→
    97→    /// Perform an update
    98→    ///
    99→    /// Returns UpdateResultPayload to send back to server
   100→    pub async fn perform_update(&self, payload: UpdatePayload) -> UpdateResultPayload {
   101→        let old_version = env!("CARGO_PKG_VERSION").to_string();
   102→
   103→        info!(
   104→            "Starting update: {} -> {} (update_id: {})",
   105→            old_version, payload.target_version, payload.update_id
   106→        );
   107→
   108→        match self.do_update(&payload, &old_version).await {
   109→            Ok(()) => {
   110→                // If we get here, something went wrong - we should have restarted
   111→                // This means the update completed but restart failed
   112→                UpdateResultPayload {
   113→                    update_id: payload.update_id,
   114→                    status: UpdateStatus::Failed,
   115→                    old_version,
   116→                    new_version: None,
   117→                    error: Some("Update installed but restart failed".into()),
   118→                }
   119→            }
   120→            Err(e) => {
   121→                error!("Update failed: {}", e);
   122→                UpdateResultPayload {
   123→                    update_id: payload.update_id,
   124→                    status: UpdateStatus::Failed,
   125→                    old_version,
   126→                    new_version: None,
   127→                    error: Some(e.to_string()),
   128→                }
   129→            }
   130→        }
   131→    }
   132→
   133→    /// Internal update implementation
   134→    async fn do_update(&self, payload: &UpdatePayload, old_version: &str) -> Result<()> {
   135→        // Step 1: Download to temp file
   136→        info!("Downloading new binary from {}", payload.download_url);
   137→        let temp_path = self.download_binary(&payload.download_url).await
   138→            .context("Failed to download binary")?;
   139→
   140→        // Step 2: Verify checksum
   141→        info!("Verifying checksum...");
   142→        self.verify_checksum(&temp_path, &payload.checksum_sha256).await
   143→            .context("Checksum verification failed")?;
   144→        info!("Checksum verified");
   145→
   146→        // Step 3: Backup current binary
   147→        info!("Backing up current binary...");
   148→        self.backup_current_binary().await
   149→            .context("Failed to backup current binary")?;
   150→
   151→        // Step 4: Save pending update info (for reconnection after restart)
   152→        info!("Saving pending update info...");
   153→        self.save_pending_update(PendingUpdateInfo {
   154→            update_id: payload.update_id,
   155→            old_version: old_version.to_string(),
   156→            target_version: payload.target_version.clone(),
   157→        }).await
   158→            .context("Failed to save pending update info")?;
   159→
   160→        // Step 5: Create rollback watchdog
   161→        info!("Creating rollback watchdog...");
   162→        self.create_rollback_watchdog().await
   163→            .context("Failed to create rollback watchdog")?;
   164→
   165→        // Step 6: Replace binary
   166→        info!("Replacing binary...");
   167→        self.replace_binary(&temp_path).await
   168→            .context("Failed to replace binary")?;
   169→
   170→        // Step 7: Restart service
   171→        info!("Restarting service...");
   172→        self.restart_service().await
   173→            .context("Failed to restart service")?;
   174→
   175→        // We should never reach here - the restart should terminate this process
   176→        Ok(())
   177→    }
   178→
   179→    /// Download the new binary to a temp file
   180→    async fn download_binary(&self, url: &str) -> Result<PathBuf> {
   181→        let response = self.http_client.get(url)
   182→            .send()
   183→            .await
   184→            .context("HTTP request failed")?;
   185→
   186→        if !response.status().is_success() {
   187→            anyhow::bail!("Download failed with status: {}", response.status());
   188→        }
   189→
   190→        let temp_path = std::env::temp_dir().join(format!("gururmm-update-{}", Uuid::new_v4()));
   191→        let mut file = fs::File::create(&temp_path).await
   192→            .context("Failed to create temp file")?;
   193→
   194→        let bytes = response.bytes().await
   195→            .context("Failed to read response body")?;
   196→
   197→        file.write_all(&bytes).await
   198→            .context("Failed to write to temp file")?;
   199→        file.flush().await?;
   200→
   201→        debug!("Downloaded {} bytes to {:?}", bytes.len(), temp_path);
   202→        Ok(temp_path)
   203→    }
   204→
   205→    /// Verify SHA256 checksum of downloaded file
   206→    async fn verify_checksum(&self, path: &Path, expected: &str) -> Result<()> {
   207→        let bytes = fs::read(path).await
   208→            .context("Failed to read file for checksum")?;
   209→
   210→        let mut hasher = Sha256::new();
   211→        hasher.update(&bytes);
   212→        let actual = format!("{:x}", hasher.finalize());
   213→
   214→        if actual.to_lowercase() != expected.to_lowercase() {
   215→            anyhow::bail!(
   216→                "Checksum mismatch: expected {}, got {}",
   217→                expected.to_lowercase(),
   218→                actual.to_lowercase()
   219→            );
   220→        }
   221→
   222→        Ok(())
   223→    }
   224→
   225→    /// Backup the current binary
   226→    async fn backup_current_binary(&self) -> Result<()> {
   227→        let backup_path = self.config.backup_path();
   228→
   229→        // Ensure config directory exists
   230→        if let Some(parent) = backup_path.parent() {
   231→            fs::create_dir_all(parent).await.ok();
   232→        }
   233→
   234→        // Copy current binary to backup location
   235→        fs::copy(&self.config.binary_path, &backup_path).await
   236→            .context("Failed to copy binary to backup")?;
   237→
   238→        debug!("Backed up to {:?}", backup_path);
   239→        Ok(())
   240→    }
   241→
   242→    /// Save pending update info to disk
   243→    async fn save_pending_update(&self, info: PendingUpdateInfo) -> Result<()> {
   244→        let path = self.config.pending_update_path();
   245→        let json = serde_json::to_string(&info)?;
   246→        fs::write(&path, json).await?;
   247→        Ok(())
   248→    }
   249→
   250→    /// Load pending update info from disk (called on startup)
   251→    pub async fn load_pending_update(config: &UpdaterConfig) -> Option<PendingUpdateInfo> {
   252→        let path = config.pending_update_path();
   253→        if let Ok(json) = fs::read_to_string(&path).await {
   254→            if let Ok(info) = serde_json::from_str(&json) {
   255→                // Clear the file after loading
   256→                let _ = fs::remove_file(&path).await;
   257→                return Some(info);
   258→            }
   259→        }
   260→        None
   261→    }
   262→
   263→    /// Create a rollback watchdog that will restore the backup if agent fails to start
   264→    async fn create_rollback_watchdog(&self) -> Result<()> {
   265→        #[cfg(unix)]
   266→        self.create_unix_rollback_watchdog().await?;
   267→
   268→        #[cfg(windows)]
   269→        self.create_windows_rollback_watchdog().await?;
   270→
   271→        Ok(())
   272→    }
   273→
   274→    #[cfg(unix)]
   275→    async fn create_unix_rollback_watchdog(&self) -> Result<()> {
   276→        let backup_path = self.config.backup_path();
   277→        let binary_path = &self.config.binary_path;
   278→        let timeout = self.config.rollback_timeout_secs;
   279→
   280→        let script = format!(r#"#!/bin/bash
   281→# GuruRMM Rollback Watchdog
   282→# Auto-generated - will be deleted after successful update
   283→
   284→BACKUP="{backup}"
   285→BINARY="{binary}"
   286→TIMEOUT={timeout}
   287→
   288→sleep $TIMEOUT
   289→
   290→# Check if agent service is running
   291→if ! systemctl is-active --quiet gururmm-agent 2>/dev/null; then
   292→    echo "Agent not running after update, rolling back..."
   293→    if [ -f "$BACKUP" ]; then
   294→        cp "$BACKUP" "$BINARY"
   295→        chmod +x "$BINARY"
   296→        systemctl start gururmm-agent
   297→        echo "Rollback completed"
   298→    else
   299→        echo "No backup file found!"
   300→    fi
   301→fi
   302→
   303→# Clean up this script
   304→rm -f /tmp/gururmm-rollback.sh
   305→"#,
   306→            backup = backup_path.display(),
   307→            binary = binary_path.display(),
   308→            timeout = timeout
   309→        );
   310→
   311→        let script_path = PathBuf::from("/tmp/gururmm-rollback.sh");
   312→        fs::write(&script_path, script).await?;
   313→
   314→        // Make executable and run in background
   315→        tokio::process::Command::new("chmod")
   316→            .arg("+x")
   317→            .arg(&script_path)
   318→            .status()
   319→            .await?;
   320→
   321→        // Spawn as detached background process
   322→        tokio::process::Command::new("nohup")
   323→            .arg("bash")
   324→            .arg(&script_path)
   325→            .arg("&")
   326→            .spawn()
   327→            .context("Failed to spawn rollback watchdog")?;
   328→
   329→        info!("Rollback watchdog started (timeout: {}s)", timeout);
   330→        Ok(())
   331→    }
   332→
   333→    #[cfg(windows)]
   334→    async fn create_windows_rollback_watchdog(&self) -> Result<()> {
   335→        let backup_path = self.config.backup_path();
   336→        let binary_path = &self.config.binary_path;
   337→        let timeout = self.config.rollback_timeout_secs;
   338→
   339→        // Create a PowerShell script for rollback
   340→        let script = format!(r#"
   341→# GuruRMM Rollback Watchdog
   342→# Auto-generated - will be deleted after successful update
   343→
   344→$Backup = "{backup}"
   345→$Binary = "{binary}"
   346→$Timeout = {timeout}
   347→
   348→Start-Sleep -Seconds $Timeout
   349→
   350→# Check if agent service is running
   351→$service = Get-Service -Name "gururmm-agent" -ErrorAction SilentlyContinue
   352→if ($service -and $service.Status -ne 'Running') {{
   353→    Write-Host "Agent not running after update, rolling back..."
   354→    if (Test-Path $Backup) {{
   355→        Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue
   356→        Copy-Item -Path $Backup -Destination $Binary -Force
   357→        Start-Service -Name "gururmm-agent"
   358→        Write-Host "Rollback completed"
   359→    }} else {{
   360→        Write-Host "No backup file found!"
   361→    }}
   362→}}
   363→
   364→# Clean up
   365→Remove-Item -Path $MyInvocation.MyCommand.Path -Force
   366→"#,
   367→            backup = backup_path.display().to_string().replace('\\', "\\\\"),
   368→            binary = binary_path.display().to_string().replace('\\', "\\\\"),
   369→            timeout = timeout
   370→        );
   371→
   372→        let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
   373→        fs::write(&script_path, script).await?;
   374→
   375→        // Schedule a task to run the rollback script
   376→        tokio::process::Command::new("schtasks")
   377→            .args([
   378→                "/Create",
   379→                "/TN", "GuruRMM-Rollback",
   380→                "/TR", &format!("powershell.exe -ExecutionPolicy Bypass -File \"{}\"", script_path.display()),
   381→                "/SC", "ONCE",
   382→                "/ST", &Self::get_scheduled_time(timeout),
   383→                "/F",
   384→            ])
   385→            .status()
   386→            .await?;
   387→
   388→        info!("Rollback watchdog scheduled (timeout: {}s)", timeout);
   389→        Ok(())
   390→    }
   391→
   392→    #[cfg(windows)]
   393→    fn get_scheduled_time(seconds_from_now: u64) -> String {
   394→        use chrono::Local;
   395→        let now = Local::now();
   396→        let scheduled = now + chrono::Duration::seconds(seconds_from_now as i64);
   397→        scheduled.format("%H:%M").to_string()
   398→    }
   399→
   400→    /// Replace the binary with the new one
   401→    async fn replace_binary(&self, new_binary: &Path) -> Result<()> {
   402→        #[cfg(unix)]
   403→        {
   404→            info!(
   405→                "Replacing binary: source={:?}, dest={:?}",
   406→                new_binary, self.config.binary_path
   407→            );
   408→
   409→            // Verify source exists
   410→            if !new_binary.exists() {
   411→                anyhow::bail!("Source binary does not exist: {:?}", new_binary);
   412→            }
   413→
   414→            let source_meta = fs::metadata(new_binary).await
   415→                .context("Failed to read source binary metadata")?;
   416→            info!("Source binary size: {} bytes", source_meta.len());
   417→
   418→            // Check destination directory
   419→            if let Some(parent) = self.config.binary_path.parent() {
   420→                if !parent.exists() {
   421→                    anyhow::bail!("Destination directory does not exist: {:?}", parent);
   422→                }
   423→            }
   424→
   425→            // On Unix, we cannot overwrite a running binary directly.
   426→            // We need to remove/rename the old file first, then copy the new one.
   427→            let old_path = self.config.binary_path.with_extension("old");
   428→
   429→            // Rename current binary (works even while running)
   430→            if self.config.binary_path.exists() {
   431→                info!("Renaming current binary to {:?}", old_path);
   432→                fs::rename(&self.config.binary_path, &old_path).await
   433→                    .with_context(|| format!(
   434→                        "Failed to rename {:?} to {:?}",
   435→                        self.config.binary_path, old_path
   436→                    ))?;
   437→            }
   438→
   439→            // Copy new binary to destination
   440→            fs::copy(new_binary, &self.config.binary_path).await
   441→                .with_context(|| format!(
   442→                    "Failed to copy {:?} to {:?}",
   443→                    new_binary, self.config.binary_path
   444→                ))?;
   445→
   446→            info!("Binary copied successfully, setting executable permissions");
   447→
   448→            // Make executable
   449→            let chmod_status = tokio::process::Command::new("chmod")
   450→                .arg("+x")
   451→                .arg(&self.config.binary_path)
   452→                .status()
   453→                .await
   454→                .context("Failed to run chmod")?;
   455→
   456→            if !chmod_status.success() {
   457→                warn!("chmod returned non-zero exit code: {:?}", chmod_status.code());
   458→            }
   459→
   460→            // Clean up old binary
   461→            fs::remove_file(&old_path).await.ok();
   462→            info!("Old binary cleaned up");
   463→        }
   464→
   465→        #[cfg(windows)]
   466→        {
   467→            // On Windows, rename the current binary first
   468→            let old_path = self.config.binary_path.with_extension("old");
   469→            fs::rename(&self.config.binary_path, &old_path).await.ok();
   470→            fs::copy(new_binary, &self.config.binary_path).await
   471→                .context("Failed to copy new binary")?;
   472→            fs::remove_file(&old_path).await.ok();
   473→        }
   474→
   475→        // Clean up temp file
   476→        fs::remove_file(new_binary).await.ok();
   477→
   478→        Ok(())
   479→    }
   480→
   481→    /// Restart the agent service
   482→    async fn restart_service(&self) -> Result<()> {
   483→        #[cfg(unix)]
   484→        {
   485→            // Try systemctl first
   486→            let status = tokio::process::Command::new("systemctl")
   487→                .args(["restart", "gururmm-agent"])
   488→                .status()
   489→                .await;
   490→
   491→            if status.is_err() || !status.unwrap().success() {
   492→                // Fallback: exec the new binary directly
   493→                warn!("systemctl restart failed, attempting direct restart");
   494→                std::process::Command::new(&self.config.binary_path)
   495→                    .spawn()
   496→                    .context("Failed to spawn new agent")?;
   497→            }
   498→        }
   499→
   500→        #[cfg(windows)]
   501→        {
   502→            // Restart Windows service
   503→            tokio::process::Command::new("sc.exe")
   504→                .args(["stop", "gururmm-agent"])
   505→                .status()
   506→                .await?;
   507→
   508→            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
   509→
   510→            tokio::process::Command::new("sc.exe")
   511→                .args(["start", "gururmm-agent"])
   512→                .status()
   513→                .await?;
   514→        }
   515→
   516→        // Give the new process a moment to start
   517→        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
   518→
   519→        // Exit this process - the new version should be running now
   520→        std::process::exit(0);
   521→    }
   522→
   523→    /// Cancel the rollback watchdog (called when update is confirmed successful)
   524→    pub async fn cancel_rollback_watchdog(&self) {
   525→        #[cfg(unix)]
   526→        {
   527→            // Kill the watchdog script
   528→            let _ = tokio::process::Command::new("pkill")
   529→                .args(["-f", "gururmm-rollback.sh"])
   530→                .status()
   531→                .await;
   532→            let _ = fs::remove_file("/tmp/gururmm-rollback.sh").await;
   533→        }
   534→
   535→        #[cfg(windows)]
   536→        {
   537→            // Delete the scheduled task
   538→            let _ = tokio::process::Command::new("schtasks")
   539→                .args(["/Delete", "/TN", "GuruRMM-Rollback", "/F"])
   540→                .status()
   541→                .await;
   542→            let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
   543→            let _ = fs::remove_file(script_path).await;
   544→        }
   545→
   546→        info!("Rollback watchdog cancelled");
   547→    }
   548→
   549→    /// Clean up backup files after successful update confirmation
   550→    pub async fn cleanup_backup(&self) {
   551→        let _ = fs::remove_file(self.config.backup_path()).await;
   552→        info!("Backup file cleaned up");
   553→    }
   554→}
   555→

<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>

//! Agent self-update module
//!
//! Handles downloading, verifying, and installing agent updates.
//! Features:
//! - Download new binary via HTTPS
//! - SHA256 checksum verification
//! - Atomic binary replacement
//! - Auto-rollback if agent fails to restart

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use sha2::{Sha256, Digest};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tracing::{debug, error, info, warn};
use uuid::Uuid;

use crate::transport::{UpdatePayload, UpdateResultPayload, UpdateStatus};

/// Configuration for the updater
#[derive(Debug, Clone)]
pub struct UpdaterConfig {
    /// Path to the current agent binary
    pub binary_path: PathBuf,
    /// Directory for config and backup files
    pub config_dir: PathBuf,
    /// Rollback timeout in seconds
    pub rollback_timeout_secs: u64,
}

impl Default for UpdaterConfig {
    fn default() -> Self {
        Self {
            binary_path: Self::detect_binary_path(),
            config_dir: Self::detect_config_dir(),
            rollback_timeout_secs: 180,
        }
    }
}

impl UpdaterConfig {
    /// Detect the path to the currently running binary
    fn detect_binary_path() -> PathBuf {
        std::env::current_exe().unwrap_or_else(|_| {
            #[cfg(windows)]
            { PathBuf::from(r"C:\Program Files\GuruRMM\gururmm-agent.exe") }
            #[cfg(not(windows))]
            { PathBuf::from("/usr/local/bin/gururmm-agent") }
        })
    }

    /// Detect the config directory
    fn detect_config_dir() -> PathBuf {
        #[cfg(windows)]
        { PathBuf::from(r"C:\ProgramData\GuruRMM") }
        #[cfg(not(windows))]
        { PathBuf::from("/etc/gururmm") }
    }

    /// Get the backup binary path
    pub fn backup_path(&self) -> PathBuf {
        self.config_dir.join("gururmm-agent.backup")
    }

    /// Get the pending update info path (stores update_id for reconnection)
    pub fn pending_update_path(&self) -> PathBuf {
        self.config_dir.join("pending-update.json")
    }
}

/// Pending update information (persisted to disk before restart)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PendingUpdateInfo {
    pub update_id: Uuid,
    pub old_version: String,
    pub target_version: String,
}

/// Agent updater
pub struct AgentUpdater {
    config: UpdaterConfig,
    http_client: reqwest::Client,
}

impl AgentUpdater {
    /// Create a new updater
    pub fn new(config: UpdaterConfig) -> Self {
        let http_client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(300))
            .build()
            .expect("Failed to create HTTP client");

        Self { config, http_client }
    }

    /// Perform an update
    ///
    /// Returns UpdateResultPayload to send back to server
    pub async fn perform_update(&self, payload: UpdatePayload) -> UpdateResultPayload {
        let old_version = env!("CARGO_PKG_VERSION").to_string();

        info!(
            "Starting update: {} -> {} (update_id: {})",
            old_version, payload.target_version, payload.update_id
        );

        match self.do_update(&payload, &old_version).await {
            Ok(()) => {
                // If we get here, something went wrong - we should have restarted
                // This means the update completed but restart failed
                UpdateResultPayload {
                    update_id: payload.update_id,
                    status: UpdateStatus::Failed,
                    old_version,
                    new_version: None,
                    error: Some("Update installed but restart failed".into()),
                }
            }
            Err(e) => {
                error!("Update failed: {}", e);
                UpdateResultPayload {
                    update_id: payload.update_id,
                    status: UpdateStatus::Failed,
                    old_version,
                    new_version: None,
                    error: Some(e.to_string()),
                }
            }
        }
    }

    /// Internal update implementation
    async fn do_update(&self, payload: &UpdatePayload, old_version: &str) -> Result<()> {
        // Step 1: Download to temp file
        info!("Downloading new binary from {}", payload.download_url);
        let temp_path = self.download_binary(&payload.download_url).await
            .context("Failed to download binary")?;

        // Step 2: Verify checksum
        info!("Verifying checksum...");
        self.verify_checksum(&temp_path, &payload.checksum_sha256).await
            .context("Checksum verification failed")?;
        info!("Checksum verified");

        // Step 3: Backup current binary
        info!("Backing up current binary...");
        self.backup_current_binary().await
            .context("Failed to backup current binary")?;

        // Step 4: Save pending update info (for reconnection after restart)
        info!("Saving pending update info...");
        self.save_pending_update(PendingUpdateInfo {
            update_id: payload.update_id,
            old_version: old_version.to_string(),
            target_version: payload.target_version.clone(),
        }).await
            .context("Failed to save pending update info")?;

        // Step 5: Create rollback watchdog
        info!("Creating rollback watchdog...");
        self.create_rollback_watchdog().await
            .context("Failed to create rollback watchdog")?;

        // Step 6: Replace binary
        info!("Replacing binary...");
        self.replace_binary(&temp_path).await
            .context("Failed to replace binary")?;

        // Step 7: Restart service
        info!("Restarting service...");
        self.restart_service().await
            .context("Failed to restart service")?;

        // We should never reach here - the restart should terminate this process
        Ok(())
    }

    /// Download the new binary to a temp file
    async fn download_binary(&self, url: &str) -> Result<PathBuf> {
        let response = self.http_client.get(url)
            .send()
            .await
            .context("HTTP request failed")?;

        if !response.status().is_success() {
            anyhow::bail!("Download failed with status: {}", response.status());
        }

        let temp_path = std::env::temp_dir().join(format!("gururmm-update-{}", Uuid::new_v4()));
        let mut file = fs::File::create(&temp_path).await
            .context("Failed to create temp file")?;

        let bytes = response.bytes().await
            .context("Failed to read response body")?;

        file.write_all(&bytes).await
            .context("Failed to write to temp file")?;
        file.flush().await?;

        debug!("Downloaded {} bytes to {:?}", bytes.len(), temp_path);
        Ok(temp_path)
    }

    /// Verify SHA256 checksum of downloaded file
    async fn verify_checksum(&self, path: &Path, expected: &str) -> Result<()> {
        let bytes = fs::read(path).await
            .context("Failed to read file for checksum")?;

        let mut hasher = Sha256::new();
        hasher.update(&bytes);
        let actual = format!("{:x}", hasher.finalize());

        if actual.to_lowercase() != expected.to_lowercase() {
            anyhow::bail!(
                "Checksum mismatch: expected {}, got {}",
                expected.to_lowercase(),
                actual.to_lowercase()
            );
        }

        Ok(())
    }

    /// Backup the current binary
    async fn backup_current_binary(&self) -> Result<()> {
        let backup_path = self.config.backup_path();

        // Ensure config directory exists
        if let Some(parent) = backup_path.parent() {
            fs::create_dir_all(parent).await.ok();
        }

        // Copy current binary to backup location
        fs::copy(&self.config.binary_path, &backup_path).await
            .context("Failed to copy binary to backup")?;

        debug!("Backed up to {:?}", backup_path);
        Ok(())
    }

    /// Save pending update info to disk
    async fn save_pending_update(&self, info: PendingUpdateInfo) -> Result<()> {
        let path = self.config.pending_update_path();
        let json = serde_json::to_string(&info)?;
        fs::write(&path, json).await?;
        Ok(())
    }

    /// Load pending update info from disk (called on startup)
    pub async fn load_pending_update(config: &UpdaterConfig) -> Option<PendingUpdateInfo> {
        let path = config.pending_update_path();
        if let Ok(json) = fs::read_to_string(&path).await {
            if let Ok(info) = serde_json::from_str(&json) {
                // Clear the file after loading
                let _ = fs::remove_file(&path).await;
                return Some(info);
            }
        }
        None
    }

    /// Create a rollback watchdog that will restore the backup if agent fails to start
    async fn create_rollback_watchdog(&self) -> Result<()> {
        #[cfg(unix)]
        self.create_unix_rollback_watchdog().await?;

        #[cfg(windows)]
        self.create_windows_rollback_watchdog().await?;

        Ok(())
    }

    #[cfg(unix)]
    async fn create_unix_rollback_watchdog(&self) -> Result<()> {
        let backup_path = self.config.backup_path();
        let binary_path = &self.config.binary_path;
        let timeout = self.config.rollback_timeout_secs;

        let script = format!(r#"#!/bin/bash
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update

BACKUP="{backup}"
BINARY="{binary}"
TIMEOUT={timeout}

sleep $TIMEOUT

# Check if agent service is running
if ! systemctl is-active --quiet gururmm-agent 2>/dev/null; then
    echo "Agent not running after update, rolling back..."
    if [ -f "$BACKUP" ]; then
        cp "$BACKUP" "$BINARY"
        chmod +x "$BINARY"
        systemctl start gururmm-agent
        echo "Rollback completed"
    else
        echo "No backup file found!"
    fi
fi

# Clean up this script
rm -f /tmp/gururmm-rollback.sh
"#,
            backup = backup_path.display(),
            binary = binary_path.display(),
            timeout = timeout
        );

        let script_path = PathBuf::from("/tmp/gururmm-rollback.sh");
        fs::write(&script_path, script).await?;

        // Make executable and run in background
        tokio::process::Command::new("chmod")
            .arg("+x")
            .arg(&script_path)
            .status()
            .await?;

        // Spawn as detached background process
        tokio::process::Command::new("nohup")
            .arg("bash")
            .arg(&script_path)
            .arg("&")
            .spawn()
            .context("Failed to spawn rollback watchdog")?;

        info!("Rollback watchdog started (timeout: {}s)", timeout);
        Ok(())
    }

    #[cfg(windows)]
    async fn create_windows_rollback_watchdog(&self) -> Result<()> {
        let backup_path = self.config.backup_path();
        let binary_path = &self.config.binary_path;
        let timeout = self.config.rollback_timeout_secs;

        // Create a PowerShell script for rollback
        let script = format!(r#"
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update

$Backup = "{backup}"
$Binary = "{binary}"
$Timeout = {timeout}

Start-Sleep -Seconds $Timeout

# Check if agent service is running
$service = Get-Service -Name "gururmm-agent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -ne 'Running') {{
    Write-Host "Agent not running after update, rolling back..."
    if (Test-Path $Backup) {{
        Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue
        Copy-Item -Path $Backup -Destination $Binary -Force
        Start-Service -Name "gururmm-agent"
        Write-Host "Rollback completed"
    }} else {{
        Write-Host "No backup file found!"
    }}
}}

# Clean up
Remove-Item -Path $MyInvocation.MyCommand.Path -Force
"#,
            backup = backup_path.display().to_string().replace('\\', "\\\\"),
            binary = binary_path.display().to_string().replace('\\', "\\\\"),
            timeout = timeout
        );

        let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
        fs::write(&script_path, script).await?;

        // Schedule a task to run the rollback script
        tokio::process::Command::new("schtasks")
            .args([
                "/Create",
                "/TN", "GuruRMM-Rollback",
                "/TR", &format!("powershell.exe -ExecutionPolicy Bypass -File \"{}\"", script_path.display()),
                "/SC", "ONCE",
                "/ST", &Self::get_scheduled_time(timeout),
                "/F",
            ])
            .status()
            .await?;

        info!("Rollback watchdog scheduled (timeout: {}s)", timeout);
        Ok(())
    }

    #[cfg(windows)]
    fn get_scheduled_time(seconds_from_now: u64) -> String {
        use chrono::Local;
        let now = Local::now();
        let scheduled = now + chrono::Duration::seconds(seconds_from_now as i64);
        scheduled.format("%H:%M").to_string()
    }

    /// Replace the binary with the new one
    async fn replace_binary(&self, new_binary: &Path) -> Result<()> {
        #[cfg(unix)]
        {
            info!(
                "Replacing binary: source={:?}, dest={:?}",
                new_binary, self.config.binary_path
            );

            // Verify source exists
            if !new_binary.exists() {
                anyhow::bail!("Source binary does not exist: {:?}", new_binary);
            }

            let source_meta = fs::metadata(new_binary).await
                .context("Failed to read source binary metadata")?;
            info!("Source binary size: {} bytes", source_meta.len());

            // Check destination directory
            if let Some(parent) = self.config.binary_path.parent() {
                if !parent.exists() {
                    anyhow::bail!("Destination directory does not exist: {:?}", parent);
                }
            }

            // On Unix, we cannot overwrite a running binary directly.
            // We need to remove/rename the old file first, then copy the new one.
            let old_path = self.config.binary_path.with_extension("old");

            // Rename current binary (works even while running)
            if self.config.binary_path.exists() {
                info!("Renaming current binary to {:?}", old_path);
                fs::rename(&self.config.binary_path, &old_path).await
                    .with_context(|| format!(
                        "Failed to rename {:?} to {:?}",
                        self.config.binary_path, old_path
                    ))?;
            }

            // Copy new binary to destination
            fs::copy(new_binary, &self.config.binary_path).await
                .with_context(|| format!(
                    "Failed to copy {:?} to {:?}",
                    new_binary, self.config.binary_path
                ))?;

            info!("Binary copied successfully, setting executable permissions");

            // Make executable
            let chmod_status = tokio::process::Command::new("chmod")
                .arg("+x")
                .arg(&self.config.binary_path)
                .status()
                .await
                .context("Failed to run chmod")?;

            if !chmod_status.success() {
                warn!("chmod returned non-zero exit code: {:?}", chmod_status.code());
            }

            // Clean up old binary
            fs::remove_file(&old_path).await.ok();
            info!("Old binary cleaned up");
        }

        #[cfg(windows)]
        {
            // On Windows, rename the current binary first
            let old_path = self.config.binary_path.with_extension("old");
            fs::rename(&self.config.binary_path, &old_path).await.ok();
            fs::copy(new_binary, &self.config.binary_path).await
                .context("Failed to copy new binary")?;
            fs::remove_file(&old_path).await.ok();
        }

        // Clean up temp file
        fs::remove_file(new_binary).await.ok();

        Ok(())
    }

    /// Restart the agent service
    async fn restart_service(&self) -> Result<()> {
        #[cfg(unix)]
        {
            // Try systemctl first
            let status = tokio::process::Command::new("systemctl")
                .args(["restart", "gururmm-agent"])
                .status()
                .await;

            if status.is_err() || !status.unwrap().success() {
                // Fallback: exec the new binary directly
                warn!("systemctl restart failed, attempting direct restart");
                std::process::Command::new(&self.config.binary_path)
                    .spawn()
                    .context("Failed to spawn new agent")?;
            }
        }

        #[cfg(windows)]
        {
            // Restart Windows service
            tokio::process::Command::new("sc.exe")
                .args(["stop", "gururmm-agent"])
                .status()
                .await?;

            tokio::time::sleep(std::time::Duration::from_secs(2)).await;

            tokio::process::Command::new("sc.exe")
                .args(["start", "gururmm-agent"])
                .status()
                .await?;
        }

        // Give the new process a moment to start
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;

        // Exit this process - the new version should be running now
        std::process::exit(0);
    }

    /// Cancel the rollback watchdog (called when update is confirmed successful)
    pub async fn cancel_rollback_watchdog(&self) {
        #[cfg(unix)]
        {
            // Kill the watchdog script
            let _ = tokio::process::Command::new("pkill")
                .args(["-f", "gururmm-rollback.sh"])
                .status()
                .await;
            let _ = fs::remove_file("/tmp/gururmm-rollback.sh").await;
        }

        #[cfg(windows)]
        {
            // Delete the scheduled task
            let _ = tokio::process::Command::new("schtasks")
                .args(["/Delete", "/TN", "GuruRMM-Rollback", "/F"])
                .status()
                .await;
            let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
            let _ = fs::remove_file(script_path).await;
        }

        info!("Rollback watchdog cancelled");
    }

    /// Clean up backup files after successful update confirmation
    pub async fn cleanup_backup(&self) {
        let _ = fs::remove_file(self.config.backup_path()).await;
        info!("Backup file cleaned up");
    }
}


Looking at the updater module, the autoupdate feature should still work. It:

1. Downloads new binary from a URL
2. Verifies SHA256 checksum
3. Backs up current binary
4. Creates rollback watchdog
5. Replaces the binary
6. Restarts the service

However, I notice one issue - the Windows service name is hardcoded as "gururmm-agent" in the restart_service and rollback watchdog, but the actual service name defined in service.rs is "GuruRMMAgent". Let me check service.rs to confirm.


The autoupdate module looks functional, but I spotted a potential issue:


26:    pub const SERVICE_NAME: &str = "GuruRMMAgent";
40:        service_dispatcher::start(SERVICE_NAME, ffi_service_main)
76:        let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
279:    const LEGACY_SERVICE_NAMES: &[&str] = &[
294:        for legacy_name in LEGACY_SERVICE_NAMES {
324:        for legacy_name in LEGACY_SERVICE_NAMES {
442:            SERVICE_NAME,
470:            name: OsString::from(SERVICE_NAME),
496:                SERVICE_NAME,
516:            println!("     Or: sc start {}", SERVICE_NAME);
550:            SERVICE_NAME,
606:            .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS)
645:            .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS)
677:        match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) {
682:                println!("Service Name: {}", SERVICE_NAME);
709:    pub const SERVICE_NAME: &str = "GuruRMMAgent";
765:        println!("   nssm install {} {:?} run --config {:?}", SERVICE_NAME, binary_dest, config_dest);
766:        println!("   nssm start {}", SERVICE_NAME);
779:        println!("   nssm stop {}", SERVICE_NAME);
780:        println!("   nssm remove {} confirm", SERVICE_NAME);
797:        println!("   nssm start {}", SERVICE_NAME);
799:        println!("   sc start {}", SERVICE_NAME);
805:        println!("   nssm stop {}", SERVICE_NAME);
807:        println!("   sc stop {}", SERVICE_NAME);
814:        println!("Service Name: {}", SERVICE_NAME);
817:        println!("   sc query {}", SERVICE_NAME);


I see the issue! The service name is "GuruRMMAgent" but the updater uses "gururmm-agent" in its restart and rollback watchdog code. This is a bug that would prevent the updater from working correctly on Windows.

Let me check the updater again and fix this.


**Bug found!** The updater uses wrong service name:

| Location | Uses | Should Be |
|----------|------|-----------|
| `updater/mod.rs:351` | `gururmm-agent` | `GuruRMMAgent` |
| `updater/mod.rs:505-511` | `gururmm-agent` | `GuruRMMAgent` |

Let me fix this:


Edit

$service = Get-Service -Name "gururmm-agent" -ErrorAction SilentlyContinue

$service = Get-Service -Name "GuruRMMAgent" -ErrorAction SilentlyContinue


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\updater\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   347→
   348→Start-Sleep -Seconds $Timeout
   349→
   350→# Check if agent service is running
   351→$service = Get-Service -Name "GuruRMMAgent" -ErrorAction SilentlyContinue
   352→if ($service -and $service.Status -ne 'Running') {{
   353→    Write-Host "Agent not running after update, rolling back..."
   354→    if (Test-Path $Backup) {{
   355→        Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue

//! Agent self-update module
//!
//! Handles downloading, verifying, and installing agent updates.
//! Features:
//! - Download new binary via HTTPS
//! - SHA256 checksum verification
//! - Atomic binary replacement
//! - Auto-rollback if agent fails to restart

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use sha2::{Sha256, Digest};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tracing::{debug, error, info, warn};
use uuid::Uuid;

use crate::transport::{UpdatePayload, UpdateResultPayload, UpdateStatus};

/// Configuration for the updater
#[derive(Debug, Clone)]
pub struct UpdaterConfig {
    /// Path to the current agent binary
    pub binary_path: PathBuf,
    /// Directory for config and backup files
    pub config_dir: PathBuf,
    /// Rollback timeout in seconds
    pub rollback_timeout_secs: u64,
}

impl Default for UpdaterConfig {
    fn default() -> Self {
        Self {
            binary_path: Self::detect_binary_path(),
            config_dir: Self::detect_config_dir(),
            rollback_timeout_secs: 180,
        }
    }
}

impl UpdaterConfig {
    /// Detect the path to the currently running binary
    fn detect_binary_path() -> PathBuf {
        std::env::current_exe().unwrap_or_else(|_| {
            #[cfg(windows)]
            { PathBuf::from(r"C:\Program Files\GuruRMM\gururmm-agent.exe") }
            #[cfg(not(windows))]
            { PathBuf::from("/usr/local/bin/gururmm-agent") }
        })
    }

    /// Detect the config directory
    fn detect_config_dir() -> PathBuf {
        #[cfg(windows)]
        { PathBuf::from(r"C:\ProgramData\GuruRMM") }
        #[cfg(not(windows))]
        { PathBuf::from("/etc/gururmm") }
    }

    /// Get the backup binary path
    pub fn backup_path(&self) -> PathBuf {
        self.config_dir.join("gururmm-agent.backup")
    }

    /// Get the pending update info path (stores update_id for reconnection)
    pub fn pending_update_path(&self) -> PathBuf {
        self.config_dir.join("pending-update.json")
    }
}

/// Pending update information (persisted to disk before restart)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PendingUpdateInfo {
    pub update_id: Uuid,
    pub old_version: String,
    pub target_version: String,
}

/// Agent updater
pub struct AgentUpdater {
    config: UpdaterConfig,
    http_client: reqwest::Client,
}

impl AgentUpdater {
    /// Create a new updater
    pub fn new(config: UpdaterConfig) -> Self {
        let http_client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(300))
            .build()
            .expect("Failed to create HTTP client");

        Self { config, http_client }
    }

    /// Perform an update
    ///
    /// Returns UpdateResultPayload to send back to server
    pub async fn perform_update(&self, payload: UpdatePayload) -> UpdateResultPayload {
        let old_version = env!("CARGO_PKG_VERSION").to_string();

        info!(
            "Starting update: {} -> {} (update_id: {})",
            old_version, payload.target_version, payload.update_id
        );

        match self.do_update(&payload, &old_version).await {
            Ok(()) => {
                // If we get here, something went wrong - we should have restarted
                // This means the update completed but restart failed
                UpdateResultPayload {
                    update_id: payload.update_id,
                    status: UpdateStatus::Failed,
                    old_version,
                    new_version: None,
                    error: Some("Update installed but restart failed".into()),
                }
            }
            Err(e) => {
                error!("Update failed: {}", e);
                UpdateResultPayload {
                    update_id: payload.update_id,
                    status: UpdateStatus::Failed,
                    old_version,
                    new_version: None,
                    error: Some(e.to_string()),
                }
            }
        }
    }

    /// Internal update implementation
    async fn do_update(&self, payload: &UpdatePayload, old_version: &str) -> Result<()> {
        // Step 1: Download to temp file
        info!("Downloading new binary from {}", payload.download_url);
        let temp_path = self.download_binary(&payload.download_url).await
            .context("Failed to download binary")?;

        // Step 2: Verify checksum
        info!("Verifying checksum...");
        self.verify_checksum(&temp_path, &payload.checksum_sha256).await
            .context("Checksum verification failed")?;
        info!("Checksum verified");

        // Step 3: Backup current binary
        info!("Backing up current binary...");
        self.backup_current_binary().await
            .context("Failed to backup current binary")?;

        // Step 4: Save pending update info (for reconnection after restart)
        info!("Saving pending update info...");
        self.save_pending_update(PendingUpdateInfo {
            update_id: payload.update_id,
            old_version: old_version.to_string(),
            target_version: payload.target_version.clone(),
        }).await
            .context("Failed to save pending update info")?;

        // Step 5: Create rollback watchdog
        info!("Creating rollback watchdog...");
        self.create_rollback_watchdog().await
            .context("Failed to create rollback watchdog")?;

        // Step 6: Replace binary
        info!("Replacing binary...");
        self.replace_binary(&temp_path).await
            .context("Failed to replace binary")?;

        // Step 7: Restart service
        info!("Restarting service...");
        self.restart_service().await
            .context("Failed to restart service")?;

        // We should never reach here - the restart should terminate this process
        Ok(())
    }

    /// Download the new binary to a temp file
    async fn download_binary(&self, url: &str) -> Result<PathBuf> {
        let response = self.http_client.get(url)
            .send()
            .await
            .context("HTTP request failed")?;

        if !response.status().is_success() {
            anyhow::bail!("Download failed with status: {}", response.status());
        }

        let temp_path = std::env::temp_dir().join(format!("gururmm-update-{}", Uuid::new_v4()));
        let mut file = fs::File::create(&temp_path).await
            .context("Failed to create temp file")?;

        let bytes = response.bytes().await
            .context("Failed to read response body")?;

        file.write_all(&bytes).await
            .context("Failed to write to temp file")?;
        file.flush().await?;

        debug!("Downloaded {} bytes to {:?}", bytes.len(), temp_path);
        Ok(temp_path)
    }

    /// Verify SHA256 checksum of downloaded file
    async fn verify_checksum(&self, path: &Path, expected: &str) -> Result<()> {
        let bytes = fs::read(path).await
            .context("Failed to read file for checksum")?;

        let mut hasher = Sha256::new();
        hasher.update(&bytes);
        let actual = format!("{:x}", hasher.finalize());

        if actual.to_lowercase() != expected.to_lowercase() {
            anyhow::bail!(
                "Checksum mismatch: expected {}, got {}",
                expected.to_lowercase(),
                actual.to_lowercase()
            );
        }

        Ok(())
    }

    /// Backup the current binary
    async fn backup_current_binary(&self) -> Result<()> {
        let backup_path = self.config.backup_path();

        // Ensure config directory exists
        if let Some(parent) = backup_path.parent() {
            fs::create_dir_all(parent).await.ok();
        }

        // Copy current binary to backup location
        fs::copy(&self.config.binary_path, &backup_path).await
            .context("Failed to copy binary to backup")?;

        debug!("Backed up to {:?}", backup_path);
        Ok(())
    }

    /// Save pending update info to disk
    async fn save_pending_update(&self, info: PendingUpdateInfo) -> Result<()> {
        let path = self.config.pending_update_path();
        let json = serde_json::to_string(&info)?;
        fs::write(&path, json).await?;
        Ok(())
    }

    /// Load pending update info from disk (called on startup)
    pub async fn load_pending_update(config: &UpdaterConfig) -> Option<PendingUpdateInfo> {
        let path = config.pending_update_path();
        if let Ok(json) = fs::read_to_string(&path).await {
            if let Ok(info) = serde_json::from_str(&json) {
                // Clear the file after loading
                let _ = fs::remove_file(&path).await;
                return Some(info);
            }
        }
        None
    }

    /// Create a rollback watchdog that will restore the backup if agent fails to start
    async fn create_rollback_watchdog(&self) -> Result<()> {
        #[cfg(unix)]
        self.create_unix_rollback_watchdog().await?;

        #[cfg(windows)]
        self.create_windows_rollback_watchdog().await?;

        Ok(())
    }

    #[cfg(unix)]
    async fn create_unix_rollback_watchdog(&self) -> Result<()> {
        let backup_path = self.config.backup_path();
        let binary_path = &self.config.binary_path;
        let timeout = self.config.rollback_timeout_secs;

        let script = format!(r#"#!/bin/bash
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update

BACKUP="{backup}"
BINARY="{binary}"
TIMEOUT={timeout}

sleep $TIMEOUT

# Check if agent service is running
if ! systemctl is-active --quiet gururmm-agent 2>/dev/null; then
    echo "Agent not running after update, rolling back..."
    if [ -f "$BACKUP" ]; then
        cp "$BACKUP" "$BINARY"
        chmod +x "$BINARY"
        systemctl start gururmm-agent
        echo "Rollback completed"
    else
        echo "No backup file found!"
    fi
fi

# Clean up this script
rm -f /tmp/gururmm-rollback.sh
"#,
            backup = backup_path.display(),
            binary = binary_path.display(),
            timeout = timeout
        );

        let script_path = PathBuf::from("/tmp/gururmm-rollback.sh");
        fs::write(&script_path, script).await?;

        // Make executable and run in background
        tokio::process::Command::new("chmod")
            .arg("+x")
            .arg(&script_path)
            .status()
            .await?;

        // Spawn as detached background process
        tokio::process::Command::new("nohup")
            .arg("bash")
            .arg(&script_path)
            .arg("&")
            .spawn()
            .context("Failed to spawn rollback watchdog")?;

        info!("Rollback watchdog started (timeout: {}s)", timeout);
        Ok(())
    }

    #[cfg(windows)]
    async fn create_windows_rollback_watchdog(&self) -> Result<()> {
        let backup_path = self.config.backup_path();
        let binary_path = &self.config.binary_path;
        let timeout = self.config.rollback_timeout_secs;

        // Create a PowerShell script for rollback
        let script = format!(r#"
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update

$Backup = "{backup}"
$Binary = "{binary}"
$Timeout = {timeout}

Start-Sleep -Seconds $Timeout

# Check if agent service is running
$service = Get-Service -Name "gururmm-agent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -ne 'Running') {{
    Write-Host "Agent not running after update, rolling back..."
    if (Test-Path $Backup) {{
        Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue
        Copy-Item -Path $Backup -Destination $Binary -Force
        Start-Service -Name "gururmm-agent"
        Write-Host "Rollback completed"
    }} else {{
        Write-Host "No backup file found!"
    }}
}}

# Clean up
Remove-Item -Path $MyInvocation.MyCommand.Path -Force
"#,
            backup = backup_path.display().to_string().replace('\\', "\\\\"),
            binary = binary_path.display().to_string().replace('\\', "\\\\"),
            timeout = timeout
        );

        let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
        fs::write(&script_path, script).await?;

        // Schedule a task to run the rollback script
        tokio::process::Command::new("schtasks")
            .args([
                "/Create",
                "/TN", "GuruRMM-Rollback",
                "/TR", &format!("powershell.exe -ExecutionPolicy Bypass -File \"{}\"", script_path.display()),
                "/SC", "ONCE",
                "/ST", &Self::get_scheduled_time(timeout),
                "/F",
            ])
            .status()
            .await?;

        info!("Rollback watchdog scheduled (timeout: {}s)", timeout);
        Ok(())
    }

    #[cfg(windows)]
    fn get_scheduled_time(seconds_from_now: u64) -> String {
        use chrono::Local;
        let now = Local::now();
        let scheduled = now + chrono::Duration::seconds(seconds_from_now as i64);
        scheduled.format("%H:%M").to_string()
    }

    /// Replace the binary with the new one
    async fn replace_binary(&self, new_binary: &Path) -> Result<()> {
        #[cfg(unix)]
        {
            info!(
                "Replacing binary: source={:?}, dest={:?}",
                new_binary, self.config.binary_path
            );

            // Verify source exists
            if !new_binary.exists() {
                anyhow::bail!("Source binary does not exist: {:?}", new_binary);
            }

            let source_meta = fs::metadata(new_binary).await
                .context("Failed to read source binary metadata")?;
            info!("Source binary size: {} bytes", source_meta.len());

            // Check destination directory
            if let Some(parent) = self.config.binary_path.parent() {
                if !parent.exists() {
                    anyhow::bail!("Destination directory does not exist: {:?}", parent);
                }
            }

            // On Unix, we cannot overwrite a running binary directly.
            // We need to remove/rename the old file first, then copy the new one.
            let old_path = self.config.binary_path.with_extension("old");

            // Rename current binary (works even while running)
            if self.config.binary_path.exists() {
                info!("Renaming current binary to {:?}", old_path);
                fs::rename(&self.config.binary_path, &old_path).await
                    .with_context(|| format!(
                        "Failed to rename {:?} to {:?}",
                        self.config.binary_path, old_path
                    ))?;
            }

            // Copy new binary to destination
            fs::copy(new_binary, &self.config.binary_path).await
                .with_context(|| format!(
                    "Failed to copy {:?} to {:?}",
                    new_binary, self.config.binary_path
                ))?;

            info!("Binary copied successfully, setting executable permissions");

            // Make executable
            let chmod_status = tokio::process::Command::new("chmod")
                .arg("+x")
                .arg(&self.config.binary_path)
                .status()
                .await
                .context("Failed to run chmod")?;

            if !chmod_status.success() {
                warn!("chmod returned non-zero exit code: {:?}", chmod_status.code());
            }

            // Clean up old binary
            fs::remove_file(&old_path).await.ok();
            info!("Old binary cleaned up");
        }

        #[cfg(windows)]
        {
            // On Windows, rename the current binary first
            let old_path = self.config.binary_path.with_extension("old");
            fs::rename(&self.config.binary_path, &old_path).await.ok();
            fs::copy(new_binary, &self.config.binary_path).await
                .context("Failed to copy new binary")?;
            fs::remove_file(&old_path).await.ok();
        }

        // Clean up temp file
        fs::remove_file(new_binary).await.ok();

        Ok(())
    }

    /// Restart the agent service
    async fn restart_service(&self) -> Result<()> {
        #[cfg(unix)]
        {
            // Try systemctl first
            let status = tokio::process::Command::new("systemctl")
                .args(["restart", "gururmm-agent"])
                .status()
                .await;

            if status.is_err() || !status.unwrap().success() {
                // Fallback: exec the new binary directly
                warn!("systemctl restart failed, attempting direct restart");
                std::process::Command::new(&self.config.binary_path)
                    .spawn()
                    .context("Failed to spawn new agent")?;
            }
        }

        #[cfg(windows)]
        {
            // Restart Windows service
            tokio::process::Command::new("sc.exe")
                .args(["stop", "gururmm-agent"])
                .status()
                .await?;

            tokio::time::sleep(std::time::Duration::from_secs(2)).await;

            tokio::process::Command::new("sc.exe")
                .args(["start", "gururmm-agent"])
                .status()
                .await?;
        }

        // Give the new process a moment to start
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;

        // Exit this process - the new version should be running now
        std::process::exit(0);
    }

    /// Cancel the rollback watchdog (called when update is confirmed successful)
    pub async fn cancel_rollback_watchdog(&self) {
        #[cfg(unix)]
        {
            // Kill the watchdog script
            let _ = tokio::process::Command::new("pkill")
                .args(["-f", "gururmm-rollback.sh"])
                .status()
                .await;
            let _ = fs::remove_file("/tmp/gururmm-rollback.sh").await;
        }

        #[cfg(windows)]
        {
            // Delete the scheduled task
            let _ = tokio::process::Command::new("schtasks")
                .args(["/Delete", "/TN", "GuruRMM-Rollback", "/F"])
                .status()
                .await;
            let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
            let _ = fs::remove_file(script_path).await;
        }

        info!("Rollback watchdog cancelled");
    }

    /// Clean up backup files after successful update confirmation
    pub async fn cleanup_backup(&self) {
        let _ = fs::remove_file(self.config.backup_path()).await;
        info!("Backup file cleaned up");
    }
}

-$service = Get-Service -Name "gururmm-agent" -ErrorAction SilentlyContinue

+$service = Get-Service -Name "GuruRMMAgent" -ErrorAction SilentlyContinue


Good, I fixed one. Now I need to fix the other references in the same file.


Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue

Stop-Service -Name "GuruRMMAgent" -Force -ErrorAction SilentlyContinue


The file C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\updater\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   351→$service = Get-Service -Name "GuruRMMAgent" -ErrorAction SilentlyContinue
   352→if ($service -and $service.Status -ne 'Running') {{
   353→    Write-Host "Agent not running after update, rolling back..."
   354→    if (Test-Path $Backup) {{
   355→        Stop-Service -Name "GuruRMMAgent" -Force -ErrorAction SilentlyContinue
   356→        Copy-Item -Path $Backup -Destination $Binary -Force
   357→        Start-Service -Name "gururmm-agent"
   358→        Write-Host "Rollback completed"
   359→    }} else {{

//! Agent self-update module
//!
//! Handles downloading, verifying, and installing agent updates.
//! Features:
//! - Download new binary via HTTPS
//! - SHA256 checksum verification
//! - Atomic binary replacement
//! - Auto-rollback if agent fails to restart

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use sha2::{Sha256, Digest};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tracing::{debug, error, info, warn};
use uuid::Uuid;

use crate::transport::{UpdatePayload, UpdateResultPayload, UpdateStatus};

/// Configuration for the updater
#[derive(Debug, Clone)]
pub struct UpdaterConfig {
    /// Path to the current agent binary
    pub binary_path: PathBuf,
    /// Directory for config and backup files
    pub config_dir: PathBuf,
    /// Rollback timeout in seconds
    pub rollback_timeout_secs: u64,
}

impl Default for UpdaterConfig {
    fn default() -> Self {
        Self {
            binary_path: Self::detect_binary_path(),
            config_dir: Self::detect_config_dir(),
            rollback_timeout_secs: 180,
        }
    }
}

impl UpdaterConfig {
    /// Detect the path to the currently running binary
    fn detect_binary_path() -> PathBuf {
        std::env::current_exe().unwrap_or_else(|_| {
            #[cfg(windows)]
            { PathBuf::from(r"C:\Program Files\GuruRMM\gururmm-agent.exe") }
            #[cfg(not(windows))]
            { PathBuf::from("/usr/local/bin/gururmm-agent") }
        })
    }

    /// Detect the config directory
    fn detect_config_dir() -> PathBuf {
        #[cfg(windows)]
        { PathBuf::from(r"C:\ProgramData\GuruRMM") }
        #[cfg(not(windows))]
        { PathBuf::from("/etc/gururmm") }
    }

    /// Get the backup binary path
    pub fn backup_path(&self) -> PathBuf {
        self.config_dir.join("gururmm-agent.backup")
    }

    /// Get the pending update info path (stores update_id for reconnection)
    pub fn pending_update_path(&self) -> PathBuf {
        self.config_dir.join("pending-update.json")
    }
}

/// Pending update information (persisted to disk before restart)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PendingUpdateInfo {
    pub update_id: Uuid,
    pub old_version: String,
    pub target_version: String,
}

/// Agent updater
pub struct AgentUpdater {
    config: UpdaterConfig,
    http_client: reqwest::Client,
}

impl AgentUpdater {
    /// Create a new updater
    pub fn new(config: UpdaterConfig) -> Self {
        let http_client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(300))
            .build()
            .expect("Failed to create HTTP client");

        Self { config, http_client }
    }

    /// Perform an update
    ///
    /// Returns UpdateResultPayload to send back to server
    pub async fn perform_update(&self, payload: UpdatePayload) -> UpdateResultPayload {
        let old_version = env!("CARGO_PKG_VERSION").to_string();

        info!(
            "Starting update: {} -> {} (update_id: {})",
            old_version, payload.target_version, payload.update_id
        );

        match self.do_update(&payload, &old_version).await {
            Ok(()) => {
                // If we get here, something went wrong - we should have restarted
                // This means the update completed but restart failed
                UpdateResultPayload {
                    update_id: payload.update_id,
                    status: UpdateStatus::Failed,
                    old_version,
                    new_version: None,
                    error: Some("Update installed but restart failed".into()),
                }
            }
            Err(e) => {
                error!("Update failed: {}", e);
                UpdateResultPayload {
                    update_id: payload.update_id,
                    status: UpdateStatus::Failed,
                    old_version,
                    new_version: None,
                    error: Some(e.to_string()),
                }
            }
        }
    }

    /// Internal update implementation
    async fn do_update(&self, payload: &UpdatePayload, old_version: &str) -> Result<()> {
        // Step 1: Download to temp file
        info!("Downloading new binary from {}", payload.download_url);
        let temp_path = self.download_binary(&payload.download_url).await
            .context("Failed to download binary")?;

        // Step 2: Verify checksum
        info!("Verifying checksum...");
        self.verify_checksum(&temp_path, &payload.checksum_sha256).await
            .context("Checksum verification failed")?;
        info!("Checksum verified");

        // Step 3: Backup current binary
        info!("Backing up current binary...");
        self.backup_current_binary().await
            .context("Failed to backup current binary")?;

        // Step 4: Save pending update info (for reconnection after restart)
        info!("Saving pending update info...");
        self.save_pending_update(PendingUpdateInfo {
            update_id: payload.update_id,
            old_version: old_version.to_string(),
            target_version: payload.target_version.clone(),
        }).await
            .context("Failed to save pending update info")?;

        // Step 5: Create rollback watchdog
        info!("Creating rollback watchdog...");
        self.create_rollback_watchdog().await
            .context("Failed to create rollback watchdog")?;

        // Step 6: Replace binary
        info!("Replacing binary...");
        self.replace_binary(&temp_path).await
            .context("Failed to replace binary")?;

        // Step 7: Restart service
        info!("Restarting service...");
        self.restart_service().await
            .context("Failed to restart service")?;

        // We should never reach here - the restart should terminate this process
        Ok(())
    }

    /// Download the new binary to a temp file
    async fn download_binary(&self, url: &str) -> Result<PathBuf> {
        let response = self.http_client.get(url)
            .send()
            .await
            .context("HTTP request failed")?;

        if !response.status().is_success() {
            anyhow::bail!("Download failed with status: {}", response.status());
        }

        let temp_path = std::env::temp_dir().join(format!("gururmm-update-{}", Uuid::new_v4()));
        let mut file = fs::File::create(&temp_path).await
            .context("Failed to create temp file")?;

        let bytes = response.bytes().await
            .context("Failed to read response body")?;

        file.write_all(&bytes).await
            .context("Failed to write to temp file")?;
        file.flush().await?;

        debug!("Downloaded {} bytes to {:?}", bytes.len(), temp_path);
        Ok(temp_path)
    }

    /// Verify SHA256 checksum of downloaded file
    async fn verify_checksum(&self, path: &Path, expected: &str) -> Result<()> {
        let bytes = fs::read(path).await
            .context("Failed to read file for checksum")?;

        let mut hasher = Sha256::new();
        hasher.update(&bytes);
        let actual = format!("{:x}", hasher.finalize());

        if actual.to_lowercase() != expected.to_lowercase() {
            anyhow::bail!(
                "Checksum mismatch: expected {}, got {}",
                expected.to_lowercase(),
                actual.to_lowercase()
            );
        }

        Ok(())
    }

    /// Backup the current binary
    async fn backup_current_binary(&self) -> Result<()> {
        let backup_path = self.config.backup_path();

        // Ensure config directory exists
        if let Some(parent) = backup_path.parent() {
            fs::create_dir_all(parent).await.ok();
        }

        // Copy current binary to backup location
        fs::copy(&self.config.binary_path, &backup_path).await
            .context("Failed to copy binary to backup")?;

        debug!("Backed up to {:?}", backup_path);
        Ok(())
    }

    /// Save pending update info to disk
    async fn save_pending_update(&self, info: PendingUpdateInfo) -> Result<()> {
        let path = self.config.pending_update_path();
        let json = serde_json::to_string(&info)?;
        fs::write(&path, json).await?;
        Ok(())
    }

    /// Load pending update info from disk (called on startup)
    pub async fn load_pending_update(config: &UpdaterConfig) -> Option<PendingUpdateInfo> {
        let path = config.pending_update_path();
        if let Ok(json) = fs::read_to_string(&path).await {
            if let Ok(info) = serde_json::from_str(&json) {
                // Clear the file after loading
                let _ = fs::remove_file(&path).await;
                return Some(info);
            }
        }
        None
    }

    /// Create a rollback watchdog that will restore the backup if agent fails to start
    async fn create_rollback_watchdog(&self) -> Result<()> {
        #[cfg(unix)]
        self.create_unix_rollback_watchdog().await?;

        #[cfg(windows)]
        self.create_windows_rollback_watchdog().await?;

        Ok(())
    }

    #[cfg(unix)]
    async fn create_unix_rollback_watchdog(&self) -> Result<()> {
        let backup_path = self.config.backup_path();
        let binary_path = &self.config.binary_path;
        let timeout = self.config.rollback_timeout_secs;

        let script = format!(r#"#!/bin/bash
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update

BACKUP="{backup}"
BINARY="{binary}"
TIMEOUT={timeout}

sleep $TIMEOUT

# Check if agent service is running
if ! systemctl is-active --quiet gururmm-agent 2>/dev/null; then
    echo "Agent not running after update, rolling back..."
    if [ -f "$BACKUP" ]; then
        cp "$BACKUP" "$BINARY"
        chmod +x "$BINARY"
        systemctl start gururmm-agent
        echo "Rollback completed"
    else
        echo "No backup file found!"
    fi
fi

# Clean up this script
rm -f /tmp/gururmm-rollback.sh
"#,
            backup = backup_path.display(),
            binary = binary_path.display(),
            timeout = timeout
        );

        let script_path = PathBuf::from("/tmp/gururmm-rollback.sh");
        fs::write(&script_path, script).await?;

        // Make executable and run in background
        tokio::process::Command::new("chmod")
            .arg("+x")
            .arg(&script_path)
            .status()
            .await?;

        // Spawn as detached background process
        tokio::process::Command::new("nohup")
            .arg("bash")
            .arg(&script_path)
            .arg("&")
            .spawn()
            .context("Failed to spawn rollback watchdog")?;

        info!("Rollback watchdog started (timeout: {}s)", timeout);
        Ok(())
    }

    #[cfg(windows)]
    async fn create_windows_rollback_watchdog(&self) -> Result<()> {
        let backup_path = self.config.backup_path();
        let binary_path = &self.config.binary_path;
        let timeout = self.config.rollback_timeout_secs;

        // Create a PowerShell script for rollback
        let script = format!(r#"
# GuruRMM Rollback Watchdog
# Auto-generated - will be deleted after successful update

$Backup = "{backup}"
$Binary = "{binary}"
$Timeout = {timeout}

Start-Sleep -Seconds $Timeout

# Check if agent service is running
$service = Get-Service -Name "GuruRMMAgent" -ErrorAction SilentlyContinue
if ($service -and $service.Status -ne 'Running') {{
    Write-Host "Agent not running after update, rolling back..."
    if (Test-Path $Backup) {{
        Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue
        Copy-Item -Path $Backup -Destination $Binary -Force
        Start-Service -Name "gururmm-agent"
        Write-Host "Rollback completed"
    }} else {{
        Write-Host "No backup file found!"
    }}
}}

# Clean up
Remove-Item -Path $MyInvocation.MyCommand.Path -Force
"#,
            backup = backup_path.display().to_string().replace('\\', "\\\\"),
            binary = binary_path.display().to_string().replace('\\', "\\\\"),
            timeout = timeout
        );

        let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
        fs::write(&script_path, script).await?;

        // Schedule a task to run the rollback script
        tokio::process::Command::new("schtasks")
            .args([
                "/Create",
                "/TN", "GuruRMM-Rollback",
                "/TR", &format!("powershell.exe -ExecutionPolicy Bypass -File \"{}\"", script_path.display()),
                "/SC", "ONCE",
                "/ST", &Self::get_scheduled_time(timeout),
                "/F",
            ])
            .status()
            .await?;

        info!("Rollback watchdog scheduled (timeout: {}s)", timeout);
        Ok(())
    }

    #[cfg(windows)]
    fn get_scheduled_time(seconds_from_now: u64) -> String {
        use chrono::Local;
        let now = Local::now();
        let scheduled = now + chrono::Duration::seconds(seconds_from_now as i64);
        scheduled.format("%H:%M").to_string()
    }

    /// Replace the binary with the new one
    async fn replace_binary(&self, new_binary: &Path) -> Result<()> {
        #[cfg(unix)]
        {
            info!(
                "Replacing binary: source={:?}, dest={:?}",
                new_binary, self.config.binary_path
            );

            // Verify source exists
            if !new_binary.exists() {
                anyhow::bail!("Source binary does not exist: {:?}", new_binary);
            }

            let source_meta = fs::metadata(new_binary).await
                .context("Failed to read source binary metadata")?;
            info!("Source binary size: {} bytes", source_meta.len());

            // Check destination directory
            if let Some(parent) = self.config.binary_path.parent() {
                if !parent.exists() {
                    anyhow::bail!("Destination directory does not exist: {:?}", parent);
                }
            }

            // On Unix, we cannot overwrite a running binary directly.
            // We need to remove/rename the old file first, then copy the new one.
            let old_path = self.config.binary_path.with_extension("old");

            // Rename current binary (works even while running)
            if self.config.binary_path.exists() {
                info!("Renaming current binary to {:?}", old_path);
                fs::rename(&self.config.binary_path, &old_path).await
                    .with_context(|| format!(
                        "Failed to rename {:?} to {:?}",
                        self.config.binary_path, old_path
                    ))?;
            }

            // Copy new binary to destination
            fs::copy(new_binary, &self.config.binary_path).await
                .with_context(|| format!(
                    "Failed to copy {:?} to {:?}",
                    new_binary, self.config.binary_path
                ))?;

            info!("Binary copied successfully, setting executable permissions");

            // Make executable
            let chmod_status = tokio::process::Command::new("chmod")
                .arg("+x")
                .arg(&self.config.binary_path)
                .status()
                .await
                .context("Failed to run chmod")?;

            if !chmod_status.success() {
                warn!("chmod returned non-zero exit code: {:?}", chmod_status.code());
            }

            // Clean up old binary
            fs::remove_file(&old_path).await.ok();
            info!("Old binary cleaned up");
        }

        #[cfg(windows)]
        {
            // On Windows, rename the current binary first
            let old_path = self.config.binary_path.with_extension("old");
            fs::rename(&self.config.binary_path, &old_path).await.ok();
            fs::copy(new_binary, &self.config.binary_path).await
                .context("Failed to copy new binary")?;
            fs::remove_file(&old_path).await.ok();
        }

        // Clean up temp file
        fs::remove_file(new_binary).await.ok();

        Ok(())
    }

    /// Restart the agent service
    async fn restart_service(&self) -> Result<()> {
        #[cfg(unix)]
        {
            // Try systemctl first
            let status = tokio::process::Command::new("systemctl")
                .args(["restart", "gururmm-agent"])
                .status()
                .await;

            if status.is_err() || !status.unwrap().success() {
                // Fallback: exec the new binary directly
                warn!("systemctl restart failed, attempting direct restart");
                std::process::Command::new(&self.config.binary_path)
                    .spawn()
                    .context("Failed to spawn new agent")?;
            }
        }

        #[cfg(windows)]
        {
            // Restart Windows service
            tokio::process::Command::new("sc.exe")
                .args(["stop", "gururmm-agent"])
                .status()
                .await?;

            tokio::time::sleep(std::time::Duration::from_secs(2)).await;

            tokio::process::Command::new("sc.exe")
                .args(["start", "gururmm-agent"])
                .status()
                .await?;
        }

        // Give the new process a moment to start
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;

        // Exit this process - the new version should be running now
        std::process::exit(0);
    }

    /// Cancel the rollback watchdog (called when update is confirmed successful)
    pub async fn cancel_rollback_watchdog(&self) {
        #[cfg(unix)]
        {
            // Kill the watchdog script
            let _ = tokio::process::Command::new("pkill")
                .args(["-f", "gururmm-rollback.sh"])
                .status()
                .await;
            let _ = fs::remove_file("/tmp/gururmm-rollback.sh").await;
        }

        #[cfg(windows)]
        {
            // Delete the scheduled task
            let _ = tokio::process::Command::new("schtasks")
                .args(["/Delete", "/TN", "GuruRMM-Rollback", "/F"])
                .status()
                .await;
            let script_path = std::env::temp_dir().join("gururmm-rollback.ps1");
            let _ = fs::remove_file(script_path).await;
        }

        info!("Rollback watchdog cancelled");
    }

    /// Clean up backup files after successful update confirmation
    pub async fn cleanup_backup(&self) {
        let _ = fs::remove_file(self.config.backup_path()).await;
        info!("Backup file cleaned up");
    }
}

-        Stop-Service -Name "gururmm-agent" -Force -ErrorAction SilentlyContinue

+        Stop-Service -Name "GuruRMMAgent" -Force -ErrorAction SilentlyContinue


There's another one on line 357, let me fix that.