diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs index eb4eec16..b67cb090 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs @@ -104,6 +104,19 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core private bool _isFirstConfigApply = true; private int? _deferredSecurityAuditConfigVersion; // deferred until after agent_started + // Runtime self-update trigger (hash mismatch recovery) + // Set by the agent host (Program.cs) to inject the SelfUpdater implementation since + // .Core cannot reference the .Agent exe project. Once-per-process guard via Interlocked. + private static int _integrityUpdateAttempted; // 0 = not yet, 1 = attempted + /// + /// Callback invoked by the runtime hash-mismatch path to trigger a forced self-update. + /// Host wires this to SelfUpdater.CheckAndApplyUpdateAsync(..., forceUpdate: true, + /// triggerReason: "runtime_hash_mismatch", downloadTimeoutMsOverride: 60_000). + /// Receives the backend-provided ZIP SHA-256 hash so the updater can use it as the + /// trusted-channel integrity source. + /// + public Func RuntimeSelfUpdateTriggerAsync { get; set; } + public MonitoringService(AgentConfiguration configuration, AgentLogger logger, string agentVersion, string previousExitType = "first_run", string previousCrashException = null, DateTime? lastBootTimeUtc = null) @@ -288,6 +301,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core // If the agent was self-updated on the previous run, emit a timeline event EmitSelfUpdateEventIfMarkerExists(); + // If the startup self-update was skipped (network timeout etc.), emit telemetry + // so we can measure how often this happens across the fleet. + EmitSelfUpdateSkippedEventIfMarkerExists(); + // Emit deferred security_audit (initial UnrestrictedMode) now that agent_started has its sequence if (_deferredSecurityAuditConfigVersion.HasValue) { @@ -604,6 +621,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core _ = _emergencyReporter?.TrySendAsync( AgentErrorType.IntegrityCheckFailed, $"Binary hash mismatch: expected={expectedHash.Substring(0, 12)}..., actual={actualHash.Substring(0, 12)}..."); + + // Trigger a forced self-update on hash mismatch (once per process). + // Runs as fire-and-forget on a background task so monitoring keeps running — + // on success, SelfUpdater calls Environment.Exit via RestartAgent; + // on failure we log and continue with the stale binary until next restart. + TriggerSelfUpdateOnMismatch(); } } catch (Exception ex) @@ -612,6 +635,44 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core } } + /// + /// Invokes the host-provided delegate on a + /// background task. Guarded by _integrityUpdateAttempted so we only try once + /// per agent process (prevents download loops if the update repeatedly fails). + /// + private void TriggerSelfUpdateOnMismatch() + { + if (Interlocked.Exchange(ref _integrityUpdateAttempted, 1) == 1) + { + _logger.Info("Self-update: already attempted in this process — skipping"); + return; + } + + if (RuntimeSelfUpdateTriggerAsync == null) + { + _logger.Warning("Self-update: no RuntimeSelfUpdateTriggerAsync wired by host — cannot recover from hash mismatch"); + return; + } + + // Hand the ZIP hash from the current config to the updater as trusted-channel source. + // Null is acceptable — SelfUpdater falls back to the version.json hash. + var zipHash = _remoteConfigService?.CurrentConfig?.LatestAgentSha256; + + Task.Run(async () => + { + try + { + _logger.Info("Self-update: triggering forced update due to runtime hash mismatch"); + await RuntimeSelfUpdateTriggerAsync(zipHash).ConfigureAwait(false); + _logger.Warning("Self-update: returned without restart — update did not apply, agent continues with stale binary"); + } + catch (Exception ex) + { + _logger.Error("Self-update: unexpected failure during forced update", ex); + } + }); + } + private void ApplyRuntimeSettingsFromRemoteConfig() { var config = _remoteConfigService?.CurrentConfig; @@ -729,6 +790,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core /// /// Reads the self-update marker file left by SelfUpdater, emits an agent_self_updated event, /// and deletes the marker. Best-effort — failures are logged but never block startup. + /// Includes phase timings, trigger reason, and a computed downtime (new process startup + /// time minus old process exit time) so we can aggregate update duration and event-loss + /// window across the fleet. /// private void EmitSelfUpdateEventIfMarkerExists() { @@ -739,33 +803,76 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core return; var json = File.ReadAllText(markerPath); - var marker = Newtonsoft.Json.JsonConvert.DeserializeObject>(json); - if (marker == null) + var marker = Newtonsoft.Json.Linq.JObject.Parse(json); + + var previousVersion = (string)marker["previousVersion"] ?? "unknown"; + var newVersion = (string)marker["newVersion"] ?? "unknown"; + var updatedAtUtc = (string)marker["updatedAtUtc"] ?? "unknown"; + var triggerReason = (string)marker["triggerReason"] ?? "startup"; + var exitAtUtc = (string)marker["exitAtUtc"]; + + long? ReadLong(string key) { - TryDeleteMarker(markerPath); - return; + var token = marker[key]; + if (token == null || token.Type == Newtonsoft.Json.Linq.JTokenType.Null) + return null; + try { return token.ToObject(); } + catch { return null; } } - marker.TryGetValue("previousVersion", out var previousVersion); - marker.TryGetValue("newVersion", out var newVersion); - marker.TryGetValue("updatedAtUtc", out var updatedAtUtc); + var versionCheckMs = ReadLong("versionCheckMs"); + var downloadMs = ReadLong("downloadMs"); + var zipSizeBytes = ReadLong("zipSizeBytes"); + var verifyMs = ReadLong("verifyMs"); + var extractMs = ReadLong("extractMs"); + var swapMs = ReadLong("swapMs"); + var totalUpdateMs = ReadLong("totalUpdateMs"); - _logger.Info($"Self-update detected: {previousVersion} → {newVersion}"); + // Downtime = new process start (_agentStartTimeUtc) minus old process exit. + // Measured across the PowerShell Wait-Process gap + new agent startup up to + // MonitoringService init. Closest proxy for "events potentially missed". + long? downtimeMs = null; + if (!string.IsNullOrEmpty(exitAtUtc) && + DateTime.TryParse(exitAtUtc, System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.RoundtripKind, out var exitDt)) + { + downtimeMs = (long)(_agentStartTimeUtc - exitDt).TotalMilliseconds; + } + + _logger.Info($"Self-update detected: {previousVersion} → {newVersion} (trigger={triggerReason}, downtimeMs={downtimeMs?.ToString() ?? "n/a"})"); + + var data = new Dictionary + { + { "previousVersion", previousVersion }, + { "newVersion", newVersion }, + { "updatedAtUtc", updatedAtUtc }, + { "triggerReason", triggerReason } + }; + if (exitAtUtc != null) data["exitAtUtc"] = exitAtUtc; + if (versionCheckMs.HasValue) data["versionCheckMs"] = versionCheckMs.Value; + if (downloadMs.HasValue) data["downloadMs"] = downloadMs.Value; + if (zipSizeBytes.HasValue) data["zipSizeBytes"] = zipSizeBytes.Value; + if (verifyMs.HasValue) data["verifyMs"] = verifyMs.Value; + if (extractMs.HasValue) data["extractMs"] = extractMs.Value; + if (swapMs.HasValue) data["swapMs"] = swapMs.Value; + if (totalUpdateMs.HasValue) data["totalUpdateMs"] = totalUpdateMs.Value; + if (downtimeMs.HasValue) data["downtimeMs"] = downtimeMs.Value; + + // Runtime-triggered updates are bumped to Warning so they stand out in the UI — + // startup updates stay Info because they are the normal rollout path. + var severity = string.Equals(triggerReason, "runtime_hash_mismatch", StringComparison.Ordinal) + ? EventSeverity.Warning + : EventSeverity.Info; EmitEvent(new EnrollmentEvent { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, EventType = Constants.EventTypes.AgentSelfUpdated, - Severity = EventSeverity.Info, + Severity = severity, Source = "Agent", - Message = $"Agent self-updated from {previousVersion} to {newVersion}", - Data = new Dictionary - { - { "previousVersion", previousVersion ?? "unknown" }, - { "newVersion", newVersion ?? "unknown" }, - { "updatedAtUtc", updatedAtUtc ?? "unknown" } - }, + Message = $"Agent self-updated from {previousVersion} to {newVersion} (trigger={triggerReason})", + Data = data, ImmediateUpload = true }); @@ -777,6 +884,60 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core } } + /// + /// Reads the self-update-skipped marker file left by SelfUpdater (startup trigger path only), + /// emits an agent_self_update_skipped event, and deletes the marker. Best-effort — failures + /// are logged but never block startup. + /// + private void EmitSelfUpdateSkippedEventIfMarkerExists() + { + try + { + var markerPath = Environment.ExpandEnvironmentVariables(Constants.SelfUpdateSkippedMarkerFile); + if (!File.Exists(markerPath)) + return; + + var json = File.ReadAllText(markerPath); + var marker = Newtonsoft.Json.Linq.JObject.Parse(json); + + var reason = (string)marker["reason"] ?? "unknown"; + var currentVersion = (string)marker["currentVersion"] ?? "unknown"; + var latestVersion = (string)marker["latestVersion"]; + var skippedAtUtc = (string)marker["skippedAtUtc"] ?? "unknown"; + var errorDetail = (string)marker["errorDetail"] ?? string.Empty; + + _logger.Warning($"Startup self-update skipped: reason={reason}, current={currentVersion}, latest={latestVersion ?? "n/a"}"); + + var data = new Dictionary + { + { "reason", reason }, + { "currentVersion", currentVersion }, + { "skippedAtUtc", skippedAtUtc }, + { "errorDetail", errorDetail } + }; + if (latestVersion != null) + data["latestVersion"] = latestVersion; + + EmitEvent(new EnrollmentEvent + { + SessionId = _configuration.SessionId, + TenantId = _configuration.TenantId, + EventType = Constants.EventTypes.AgentSelfUpdateSkipped, + Severity = EventSeverity.Warning, + Source = "Agent", + Message = $"Startup self-update skipped (reason={reason})", + Data = data, + ImmediateUpload = true + }); + + TryDeleteMarker(markerPath); + } + catch (Exception ex) + { + _logger.Warning($"Could not process self-update-skipped marker: {ex.Message}"); + } + } + private static void TryDeleteMarker(string path) { try { File.Delete(path); } catch { } diff --git a/src/Agent/AutopilotMonitor.Agent/Program.cs b/src/Agent/AutopilotMonitor.Agent/Program.cs index 4e526542..81cb7380 100644 --- a/src/Agent/AutopilotMonitor.Agent/Program.cs +++ b/src/Agent/AutopilotMonitor.Agent/Program.cs @@ -325,6 +325,24 @@ namespace AutopilotMonitor.Agent using (var service = new MonitoringService(config, logger, agentVersion, previousExitType, previousCrashException, lastBootUtc)) { + // Wire the runtime self-update trigger: on hash mismatch, MonitoringService + // invokes SelfUpdater with forceUpdate=true and a longer download timeout + // (60s, vs. 10s at startup) because monitoring is already running and we + // know an update is required. + service.RuntimeSelfUpdateTriggerAsync = zipHash => + { + if (!string.IsNullOrEmpty(zipHash)) + SelfUpdater.BackendExpectedSha256 = zipHash; + + return SelfUpdater.CheckAndApplyUpdateAsync( + currentVersion: agentVersion, + agentDir: agentDir, + consoleMode: consoleMode, + forceUpdate: true, + triggerReason: "runtime_hash_mismatch", + downloadTimeoutMsOverride: 60000); + }; + service.Start(); if (consoleMode) diff --git a/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs b/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs index d1e2780d..5f6b0095 100644 --- a/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs +++ b/src/Agent/AutopilotMonitor.Agent/SelfUpdater.cs @@ -91,7 +91,26 @@ namespace AutopilotMonitor.Agent /// On success, restarts the process and never returns. /// On any failure, returns normally so the current version continues. /// - public static async Task CheckAndApplyUpdateAsync(string currentVersion, string agentDir, bool consoleMode) + /// + /// When true, bypasses the IsNewerVersion check — used by the runtime-hash-mismatch trigger + /// to cover hotfixes that reuse the same version number. + /// + /// + /// "startup" (default) or "runtime_hash_mismatch". Recorded in the marker file so the + /// agent_self_updated event can distinguish the two paths. + /// + /// + /// Overrides the ZIP download timeout. Startup path leaves null (fast-fail, 10s) to preserve + /// Autopilot boot speed. Runtime trigger passes 60000 because monitoring is already running + /// and we know an update is required. + /// + public static async Task CheckAndApplyUpdateAsync( + string currentVersion, + string agentDir, + bool consoleMode, + bool forceUpdate = false, + string triggerReason = "startup", + int? downloadTimeoutMsOverride = null) { Action log = msg => { @@ -99,26 +118,54 @@ namespace AutopilotMonitor.Agent if (consoleMode) Console.WriteLine(msg); }; + var isStartupTrigger = string.Equals(triggerReason, "startup", StringComparison.Ordinal); + var downloadTimeoutMs = downloadTimeoutMsOverride ?? DownloadTimeoutMs; + var totalSw = Stopwatch.StartNew(); + long versionCheckMs = 0, downloadMs = 0, verifyMs = 0, extractMs = 0, swapMs = 0; + long zipSizeBytes = 0; + string latestVersion = null; + try { // Step 1: Fetch version.json (1s timeout) - var (latestVersion, manifestSha256) = await GetLatestVersionAsync(log); - if (latestVersion == null) - return; // Could not determine latest version — continue with current + var versionSw = Stopwatch.StartNew(); + string manifestSha256; + (latestVersion, manifestSha256) = await GetLatestVersionAsync(log); + versionSw.Stop(); + versionCheckMs = versionSw.ElapsedMilliseconds; - // Step 2: Compare versions - if (!IsNewerVersion(currentVersion, latestVersion)) + if (latestVersion == null) { - log($"Self-update: current version {currentVersion} is up to date (latest: {latestVersion})"); - return; + if (isStartupTrigger) + WriteSkipMarker("version_check_failed", currentVersion, null, "version.json fetch failed or timed out", log); + return; // Could not determine latest version — continue with current } - log($"Self-update: newer version available — current={currentVersion}, latest={latestVersion}"); + // Step 2: Compare versions (skipped when forceUpdate=true) + if (!forceUpdate && !IsNewerVersion(currentVersion, latestVersion)) + { + log($"Self-update: current version {currentVersion} is up to date (latest: {latestVersion})"); + return; // happy path — no skip marker + } - // Step 3: Download ZIP (10s timeout) + if (forceUpdate) + log($"Self-update: FORCE mode (trigger={triggerReason}) — current={currentVersion}, latest={latestVersion}, skipping version check"); + else + log($"Self-update: newer version available — current={currentVersion}, latest={latestVersion}"); + + // Step 3: Download ZIP (timeout = downloadTimeoutMs) var zipPath = Path.Combine(Path.GetTempPath(), "AutopilotMonitor-Agent-Update.zip"); - if (!await DownloadZipAsync(zipPath, log)) + var downloadTimerSw = Stopwatch.StartNew(); + var downloadOk = await DownloadZipAsync(zipPath, downloadTimeoutMs, log); + downloadTimerSw.Stop(); + downloadMs = downloadTimerSw.ElapsedMilliseconds; + if (!downloadOk) + { + if (isStartupTrigger) + WriteSkipMarker("download_failed", currentVersion, latestVersion, $"ZIP download failed within {downloadTimeoutMs}ms", log); return; + } + try { zipSizeBytes = new FileInfo(zipPath).Length; } catch { /* best-effort */ } // Step 3b: Verify SHA-256 integrity (backend hash has priority over version.json hash) string expectedSha256; @@ -137,16 +184,31 @@ namespace AutopilotMonitor.Agent expectedSha256 = null; } - if (!VerifyZipIntegrity(zipPath, expectedSha256, log)) + var verifySw = Stopwatch.StartNew(); + var integrityOk = VerifyZipIntegrity(zipPath, expectedSha256, log); + verifySw.Stop(); + verifyMs = verifySw.ElapsedMilliseconds; + + if (!integrityOk) { CleanupStaging(null, zipPath); + if (isStartupTrigger) + WriteSkipMarker("integrity_mismatch", currentVersion, latestVersion, "SHA-256 mismatch on downloaded ZIP", log); return; } // Step 4: Extract to staging directory var stagingDir = Environment.ExpandEnvironmentVariables(Constants.AgentUpdateStagingDir); - if (!ExtractToStaging(zipPath, stagingDir, log)) + var extractSw = Stopwatch.StartNew(); + var extractOk = ExtractToStaging(zipPath, stagingDir, log); + extractSw.Stop(); + extractMs = extractSw.ElapsedMilliseconds; + if (!extractOk) + { + if (isStartupTrigger) + WriteSkipMarker("extract_failed", currentVersion, latestVersion, "ZIP extraction failed", log); return; + } // Step 5: Validate staging var stagedExe = Path.Combine(stagingDir, "AutopilotMonitor.Agent.exe"); @@ -154,13 +216,21 @@ namespace AutopilotMonitor.Agent { log("Self-update: staging validation failed — AutopilotMonitor.Agent.exe not found in ZIP"); CleanupStaging(stagingDir, zipPath); + if (isStartupTrigger) + WriteSkipMarker("extract_failed", currentVersion, latestVersion, "AutopilotMonitor.Agent.exe not found in ZIP", log); return; } // Step 6: Swap files (rename locked → .old, copy new) - if (!SwapFiles(agentDir, stagingDir, log)) + var swapTimerSw = Stopwatch.StartNew(); + var swapOk = SwapFiles(agentDir, stagingDir, log); + swapTimerSw.Stop(); + swapMs = swapTimerSw.ElapsedMilliseconds; + if (!swapOk) { CleanupStaging(stagingDir, zipPath); + if (isStartupTrigger) + WriteSkipMarker("swap_failed", currentVersion, latestVersion, "file swap failed", log); return; } @@ -168,7 +238,19 @@ namespace AutopilotMonitor.Agent CleanupStaging(stagingDir, zipPath); // Step 8: Write marker file so the new agent can emit an event on next startup - WriteSelfUpdateMarker(currentVersion, latestVersion, log); + totalSw.Stop(); + WriteSelfUpdateMarker( + previousVersion: currentVersion, + newVersion: latestVersion, + triggerReason: triggerReason, + versionCheckMs: versionCheckMs, + downloadMs: downloadMs, + zipSizeBytes: zipSizeBytes, + verifyMs: verifyMs, + extractMs: extractMs, + swapMs: swapMs, + totalUpdateMs: totalSw.ElapsedMilliseconds, + log: log); // Step 9: Restart via PowerShell Wait-Process log($"Self-update: files swapped successfully, restarting agent (v{latestVersion})..."); @@ -179,6 +261,10 @@ namespace AutopilotMonitor.Agent catch (Exception ex) { log($"Self-update: unexpected error — {ex.Message}. Continuing with current version."); + if (isStartupTrigger) + { + try { WriteSkipMarker("unexpected_error", currentVersion, latestVersion, ex.Message, log); } catch { } + } } } @@ -300,7 +386,7 @@ namespace AutopilotMonitor.Agent /// /// Downloads the agent ZIP to a temp path. Returns false on failure. /// - private static async Task DownloadZipAsync(string zipPath, Action log) + private static async Task DownloadZipAsync(string zipPath, int timeoutMs, Action log) { try { @@ -311,7 +397,7 @@ namespace AutopilotMonitor.Agent File.Delete(zipPath); using (var handler = new HttpClientHandler()) - using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(DownloadTimeoutMs) }) + using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(timeoutMs) }) { using (var response = await client.GetAsync(zipUrl)) { @@ -328,7 +414,7 @@ namespace AutopilotMonitor.Agent } catch (TaskCanceledException) { - log("Self-update: ZIP download timed out (10s) — aborting update"); + log($"Self-update: ZIP download timed out ({timeoutMs}ms) — aborting update"); try { File.Delete(zipPath); } catch { } return false; } @@ -467,8 +553,21 @@ namespace AutopilotMonitor.Agent /// /// Writes a small JSON marker so the next agent startup can emit an agent_self_updated event. /// Best-effort — failure here must not block the update. + /// Includes phase timings, trigger reason, and exit timestamp so the downstream event + /// can carry aggregable telemetry (downtime, total update duration, etc.). /// - private static void WriteSelfUpdateMarker(string previousVersion, string newVersion, Action log) + private static void WriteSelfUpdateMarker( + string previousVersion, + string newVersion, + string triggerReason, + long versionCheckMs, + long downloadMs, + long zipSizeBytes, + long verifyMs, + long extractMs, + long swapMs, + long totalUpdateMs, + Action log) { try { @@ -477,8 +576,26 @@ namespace AutopilotMonitor.Agent if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); - var json = $"{{\"previousVersion\":\"{previousVersion}\",\"newVersion\":\"{newVersion}\",\"updatedAtUtc\":\"{DateTime.UtcNow:O}\"}}"; - File.WriteAllText(markerPath, json); + var now = DateTime.UtcNow; + var payload = new JObject + { + ["previousVersion"] = previousVersion ?? "unknown", + ["newVersion"] = newVersion ?? "unknown", + ["triggerReason"] = triggerReason ?? "startup", + ["updatedAtUtc"] = now.ToString("O"), + // exitAtUtc is written here — actual Environment.Exit happens a few hundred ms later + // via PowerShell Wait-Process, which is close enough for downtime telemetry. + ["exitAtUtc"] = now.ToString("O"), + ["versionCheckMs"] = versionCheckMs, + ["downloadMs"] = downloadMs, + ["zipSizeBytes"] = zipSizeBytes, + ["verifyMs"] = verifyMs, + ["extractMs"] = extractMs, + ["swapMs"] = swapMs, + ["totalUpdateMs"] = totalUpdateMs + }; + + File.WriteAllText(markerPath, payload.ToString(Newtonsoft.Json.Formatting.None)); log("Self-update: marker file written for next-startup event"); } catch (Exception ex) @@ -487,6 +604,41 @@ namespace AutopilotMonitor.Agent } } + /// + /// Writes a small JSON marker so the next agent startup can emit an agent_self_update_skipped + /// event. Only called on the startup trigger path (runtime-triggered failures are logged + /// normally because the full logger is already up). Best-effort — never throws. + /// + private static void WriteSkipMarker(string reason, string currentVersion, string latestVersion, string errorDetail, Action log) + { + try + { + var markerPath = Environment.ExpandEnvironmentVariables(Constants.SelfUpdateSkippedMarkerFile); + var dir = Path.GetDirectoryName(markerPath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + var detail = errorDetail ?? string.Empty; + if (detail.Length > 200) detail = detail.Substring(0, 200); + + var payload = new JObject + { + ["reason"] = reason ?? "unknown", + ["currentVersion"] = currentVersion ?? "unknown", + ["latestVersion"] = latestVersion, // may be null if version check failed + ["skippedAtUtc"] = DateTime.UtcNow.ToString("O"), + ["errorDetail"] = detail + }; + + File.WriteAllText(markerPath, payload.ToString(Newtonsoft.Json.Formatting.None)); + log($"Self-update: skip marker written (reason={reason})"); + } + catch (Exception ex) + { + log($"Self-update: could not write skip marker — {ex.Message} (non-critical)"); + } + } + /// /// Cleans up staging directory and temp ZIP file. /// diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 4fdb2984..a80d4992 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -887 +889 diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index f1e55a18..6040f5b6 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -35,6 +35,14 @@ namespace AutopilotMonitor.Shared /// public const string SelfUpdateMarkerFile = @"%ProgramData%\AutopilotMonitor\State\self-update-info.json"; + /// + /// Marker file written by SelfUpdater when the startup update path is skipped (network timeout, + /// download failure, integrity mismatch, etc.); read by MonitoringService on next startup to emit + /// an agent_self_update_skipped event. Only written on the startup trigger path — runtime-triggered + /// failures are logged normally because the full logger is already up. + /// + public const string SelfUpdateSkippedMarkerFile = @"%ProgramData%\AutopilotMonitor\State\self-update-skipped.json"; + /// /// Default path for the IME pattern match log file (debugging/diagnostics) /// @@ -168,6 +176,7 @@ namespace AutopilotMonitor.Shared public const string SoftwareInventoryAnalysis = "software_inventory_analysis"; public const string VulnerabilityReport = "vulnerability_report"; public const string AgentSelfUpdated = "agent_self_updated"; + public const string AgentSelfUpdateSkipped = "agent_self_update_skipped"; } // -----------------------------------------------------------------------