mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(v2core): M4.5.a restore ImeTrackerStatePersistence (peripheral capability, not dead code)
M4.5.a hat ImeTrackerStatePersistence faelschlicherweise als Legacy
geloescht. Korrektur: die Klasse persistiert den IME-Collector-internal
State, nicht DecisionEngine-State:
- LogFilePositionTracker: Byte-Offset pro IME-Logfile (ohne das
re-parst der Tracker nach Restart von Byte 0 → Event-Spam)
- SeenAppIds: Phase-Isolation (Device-Phase-Apps duerfen nicht in
AccountSetup-Phase bleeden)
- IgnoreList: App-IDs aus abgeschlossenen Phasen, die weiterhin in
Logs auftauchen koennen
- Packages (AppPackageStateList): vollstaendiger Install-Progress,
DO-Telemetry, ErrorPatterns, AppType, AttemptNumber — essentiell
fuer App-Download-Monitor, Install-Monitor, App-Summaries
- Phase-Tracking (CurrentPhaseOrder, LastEspPhaseDetected, etc.)
SignalLog-Replay deckt das NICHT ab — der SignalLog persistiert
DecisionSignals (SessionStarted, EspPhaseChanged, HelloResolved, …),
nicht per-App-Install-Progress oder Log-File-Positions.
Restore:
- ImeTrackerStatePersistence.cs wieder aus git history
- ImeLogTracker.cs: _statePersistence-Feld + Ctor-Init +
LoadState/SaveState/DeleteState-Bodies aus 8ca845c^ restored
(nur die beiden dead using-Imports auf Flows/Completion bleiben
entfernt, da deren Namespaces tatsaechlich geloescht sind)
Build + Tests gruen (286/287, known pre-existing Flake bleibt fuer
M4.5.c).
Danke an User fuer den Catch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+155
-16
@@ -72,9 +72,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _allAppsCompletedFired;
|
||||
|
||||
// M4.5.a: ImeTrackerStatePersistence removed. V2 relies on SignalLog-replay + the
|
||||
// SignalAdapter's fire-once dedup for resume semantics; fresh-start on restart is
|
||||
// intentional. See plan §4.x M4.5 Legacy-Cleanup.
|
||||
// State persistence: saves tracker state to disk so agent restart continues
|
||||
// from the exact log position without re-parsing or re-building ignore lists.
|
||||
private readonly ImeTrackerStatePersistence _statePersistence;
|
||||
private bool _stateDirty;
|
||||
|
||||
// Standard GUID capture pattern used as {GUID} placeholder in patterns
|
||||
@@ -148,9 +148,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
|
||||
_matchLogPath = matchLogPath;
|
||||
_packageStates = new AppPackageStateList(logger);
|
||||
|
||||
// M4.5.a: state-persistence removed; stateDirectory retained in signature for
|
||||
// backwards-compat with existing callers/tests but intentionally unused.
|
||||
_ = stateDirectory;
|
||||
// State persistence setup
|
||||
if (!string.IsNullOrEmpty(stateDirectory))
|
||||
{
|
||||
_statePersistence = new ImeTrackerStatePersistence(stateDirectory, logger);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_matchLogPath))
|
||||
{
|
||||
@@ -282,27 +284,164 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
|
||||
_pollingTask = null;
|
||||
}
|
||||
|
||||
#region State Persistence (M4.5.a: no-op stubs — legacy ImeTrackerStatePersistence removed)
|
||||
|
||||
// V2 resume semantics come from SignalLog-replay + SignalAdapter fire-once dedup.
|
||||
// These stubs preserve the call-sites (LoadState / SaveState / DeleteState) across
|
||||
// the polling loop + event handlers so the tracker compiles unchanged outside this
|
||||
// region. See plan §4.x M4.5 Legacy-Cleanup.
|
||||
#region State Persistence
|
||||
|
||||
/// <summary>
|
||||
/// Loads persisted state from disk. Called on Start() to restore tracker state
|
||||
/// after an agent restart, so parsing continues exactly where it left off.
|
||||
/// </summary>
|
||||
private void LoadState()
|
||||
{
|
||||
// no-op — fresh start on restart
|
||||
if (_statePersistence == null) return;
|
||||
|
||||
var state = _statePersistence.Load();
|
||||
if (state == null) return;
|
||||
|
||||
// Restore phase tracking
|
||||
_currentPhaseOrder = state.CurrentPhaseOrder;
|
||||
_lastEspPhaseDetected = state.LastEspPhaseDetected;
|
||||
_allAppsCompletedFired = state.AllAppsCompletedFired;
|
||||
_logPhaseIsCurrentPhase = state.LogPhaseIsCurrentPhase;
|
||||
|
||||
// Restore seen app IDs
|
||||
_seenAppIds.Clear();
|
||||
if (state.SeenAppIds != null)
|
||||
{
|
||||
foreach (var id in state.SeenAppIds)
|
||||
_seenAppIds.Add(id);
|
||||
}
|
||||
|
||||
// Restore ignore list
|
||||
if (state.IgnoreList != null)
|
||||
{
|
||||
foreach (var id in state.IgnoreList)
|
||||
_packageStates.AddToIgnoreList(id);
|
||||
}
|
||||
|
||||
// Restore current package ID
|
||||
_packageStates.CurrentPackageId = state.CurrentPackageId;
|
||||
|
||||
// Restore package states
|
||||
if (state.Packages != null)
|
||||
{
|
||||
_packageStates.Clear();
|
||||
foreach (var p in state.Packages)
|
||||
{
|
||||
var pkg = AppPackageState.Restore(
|
||||
p.Id, p.ListPos, p.Name,
|
||||
(AppRunAs)p.RunAs, (AppIntent)p.Intent, (AppTargeted)p.Targeted,
|
||||
p.DependsOn != null ? new HashSet<string>(p.DependsOn) : new HashSet<string>(),
|
||||
(AppInstallationState)p.InstallationState, p.DownloadingOrInstallingSeen,
|
||||
p.ProgressPercent, p.BytesDownloaded, p.BytesTotal,
|
||||
errorPatternId: p.ErrorPatternId, errorDetail: p.ErrorDetail, errorCode: p.ErrorCode,
|
||||
exitCode: p.ExitCode, hresultFromWin32: p.HResultFromWin32,
|
||||
doFileSize: p.DoFileSize, doTotalBytesDownloaded: p.DoTotalBytesDownloaded,
|
||||
doBytesFromPeers: p.DoBytesFromPeers, doPercentPeerCaching: p.DoPercentPeerCaching,
|
||||
doBytesFromLanPeers: p.DoBytesFromLanPeers, doBytesFromGroupPeers: p.DoBytesFromGroupPeers,
|
||||
doBytesFromInternetPeers: p.DoBytesFromInternetPeers, doDownloadMode: p.DoDownloadMode,
|
||||
doDownloadDuration: p.DoDownloadDuration, doBytesFromHttp: p.DoBytesFromHttp,
|
||||
hasDoTelemetry: p.HasDoTelemetry,
|
||||
appVersion: p.AppVersion, appType: p.AppType,
|
||||
attemptNumber: p.AttemptNumber, detectionResult: p.DetectionResult);
|
||||
_packageStates.Add(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore file positions
|
||||
if (state.FilePositions != null)
|
||||
{
|
||||
foreach (var kvp in state.FilePositions)
|
||||
{
|
||||
var fullPath = Path.Combine(_logFolder, kvp.Key);
|
||||
_positionTracker.RestorePosition(fullPath, kvp.Value.Position, kvp.Value.LastKnownSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-activate patterns based on restored phase state
|
||||
ActivatePatterns(_logPhaseIsCurrentPhase, force: true);
|
||||
|
||||
_logger.Info($"ImeLogTracker: state restored - phase: {_lastEspPhaseDetected ?? "(none)"} (order: {_currentPhaseOrder}), " +
|
||||
$"ignore list: {_packageStates.IgnoreList.Count}, packages: {_packageStates.Count}, " +
|
||||
$"file positions: {state.FilePositions?.Count ?? 0}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists current tracker state to disk as JSON.
|
||||
/// Called after each polling cycle when state has changed.
|
||||
/// </summary>
|
||||
private void SaveState()
|
||||
{
|
||||
// no-op — nothing to persist
|
||||
if (_statePersistence == null) return;
|
||||
|
||||
// Build state DTO
|
||||
var state = new ImeTrackerStateData
|
||||
{
|
||||
CurrentPhaseOrder = _currentPhaseOrder,
|
||||
LastEspPhaseDetected = _lastEspPhaseDetected,
|
||||
AllAppsCompletedFired = _allAppsCompletedFired,
|
||||
LogPhaseIsCurrentPhase = _logPhaseIsCurrentPhase,
|
||||
SeenAppIds = _seenAppIds.ToList(),
|
||||
IgnoreList = _packageStates.IgnoreList.ToList(),
|
||||
CurrentPackageId = _packageStates.CurrentPackageId,
|
||||
Packages = _packageStates.Select(p => new PackageStateData
|
||||
{
|
||||
Id = p.Id,
|
||||
ListPos = p.ListPos,
|
||||
Name = p.Name,
|
||||
RunAs = (int)p.RunAs,
|
||||
Intent = (int)p.Intent,
|
||||
Targeted = (int)p.Targeted,
|
||||
DependsOn = p.DependsOn?.ToList() ?? new List<string>(),
|
||||
InstallationState = (int)p.InstallationState,
|
||||
DownloadingOrInstallingSeen = p.DownloadingOrInstallingSeen,
|
||||
ProgressPercent = p.ProgressPercent,
|
||||
BytesDownloaded = p.BytesDownloaded,
|
||||
BytesTotal = p.BytesTotal,
|
||||
ErrorPatternId = p.ErrorPatternId,
|
||||
ErrorDetail = p.ErrorDetail,
|
||||
ErrorCode = p.ErrorCode,
|
||||
ExitCode = p.ExitCode,
|
||||
HResultFromWin32 = p.HResultFromWin32,
|
||||
DoFileSize = p.DoFileSize,
|
||||
DoTotalBytesDownloaded = p.DoTotalBytesDownloaded,
|
||||
DoBytesFromPeers = p.DoBytesFromPeers,
|
||||
DoPercentPeerCaching = p.DoPercentPeerCaching,
|
||||
DoBytesFromLanPeers = p.DoBytesFromLanPeers,
|
||||
DoBytesFromGroupPeers = p.DoBytesFromGroupPeers,
|
||||
DoBytesFromInternetPeers = p.DoBytesFromInternetPeers,
|
||||
DoDownloadMode = p.DoDownloadMode,
|
||||
DoDownloadDuration = p.DoDownloadDuration,
|
||||
DoBytesFromHttp = p.DoBytesFromHttp,
|
||||
HasDoTelemetry = p.HasDoTelemetry,
|
||||
AppVersion = p.AppVersion,
|
||||
AppType = p.AppType,
|
||||
AttemptNumber = p.AttemptNumber,
|
||||
DetectionResult = p.DetectionResult
|
||||
}).ToList(),
|
||||
FilePositions = new Dictionary<string, FilePositionData>()
|
||||
};
|
||||
|
||||
// Store file positions by filename only (log folder is known)
|
||||
foreach (var kvp in _positionTracker.GetAllPositions())
|
||||
{
|
||||
var fileName = Path.GetFileName(kvp.Key);
|
||||
state.FilePositions[fileName] = new FilePositionData
|
||||
{
|
||||
Position = kvp.Value.Position,
|
||||
LastKnownSize = kvp.Value.LastKnownSize
|
||||
};
|
||||
}
|
||||
|
||||
_statePersistence.Save(state);
|
||||
}
|
||||
|
||||
/// <summary>Kept as public no-op for API compatibility; callers treat it as a hint.</summary>
|
||||
/// <summary>
|
||||
/// Deletes persisted state file. Called on enrollment complete to ensure
|
||||
/// a fresh state on the next enrollment cycle.
|
||||
/// </summary>
|
||||
public void DeleteState()
|
||||
{
|
||||
// no-op
|
||||
_statePersistence?.Delete();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles persistence of ImeLogTracker state to disk (JSON serialization).
|
||||
/// Extracted from ImeLogTracker to separate I/O concerns from log parsing.
|
||||
/// </summary>
|
||||
public class ImeTrackerStatePersistence
|
||||
{
|
||||
private readonly string _stateDirectory;
|
||||
private readonly string _stateFilePath;
|
||||
private readonly AgentLogger _logger;
|
||||
|
||||
public ImeTrackerStatePersistence(string stateDirectory, AgentLogger logger)
|
||||
{
|
||||
_stateDirectory = Environment.ExpandEnvironmentVariables(stateDirectory);
|
||||
_stateFilePath = Path.Combine(_stateDirectory, "ime-tracker-state.json");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string StateFilePath => _stateFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Loads persisted state from disk. Returns null if no state file exists or on error.
|
||||
/// </summary>
|
||||
public ImeTrackerStateData Load()
|
||||
{
|
||||
if (!File.Exists(_stateFilePath))
|
||||
{
|
||||
_logger.Info("ImeLogTracker: no persisted state found (fresh enrollment)");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_stateFilePath);
|
||||
var state = JsonConvert.DeserializeObject<ImeTrackerStateData>(json);
|
||||
if (state == null)
|
||||
{
|
||||
_logger.Warning("ImeLogTracker: persisted state file was empty or invalid");
|
||||
return null;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"ImeLogTracker: failed to load persisted state, starting fresh: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists tracker state to disk as JSON (atomic write via temp file).
|
||||
/// </summary>
|
||||
public void Save(ImeTrackerStateData state)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_stateDirectory);
|
||||
var tempPath = _stateFilePath + ".tmp";
|
||||
File.WriteAllText(tempPath, JsonConvert.SerializeObject(state, Formatting.Indented));
|
||||
File.Copy(tempPath, _stateFilePath, overwrite: true);
|
||||
try { File.Delete(tempPath); } catch { }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"ImeLogTracker: failed to save state: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes persisted state file.
|
||||
/// </summary>
|
||||
public void Delete()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_stateFilePath))
|
||||
{
|
||||
File.Delete(_stateFilePath);
|
||||
_logger.Info("ImeLogTracker: persisted state deleted");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"ImeLogTracker: failed to delete state file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- DTOs for state serialization ----
|
||||
|
||||
public class ImeTrackerStateData
|
||||
{
|
||||
public int CurrentPhaseOrder { get; set; }
|
||||
public string LastEspPhaseDetected { get; set; }
|
||||
public bool AllAppsCompletedFired { get; set; }
|
||||
public bool LogPhaseIsCurrentPhase { get; set; }
|
||||
public List<string> SeenAppIds { get; set; }
|
||||
public List<string> IgnoreList { get; set; }
|
||||
public string CurrentPackageId { get; set; }
|
||||
public List<PackageStateData> Packages { get; set; }
|
||||
public Dictionary<string, FilePositionData> FilePositions { get; set; }
|
||||
}
|
||||
|
||||
public class PackageStateData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public int ListPos { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int RunAs { get; set; }
|
||||
public int Intent { get; set; }
|
||||
public int Targeted { get; set; }
|
||||
public List<string> DependsOn { get; set; }
|
||||
public int InstallationState { get; set; }
|
||||
public bool DownloadingOrInstallingSeen { get; set; }
|
||||
public int? ProgressPercent { get; set; }
|
||||
public long BytesDownloaded { get; set; }
|
||||
public long BytesTotal { get; set; }
|
||||
|
||||
// Error context
|
||||
public string ErrorPatternId { get; set; }
|
||||
public string ErrorDetail { get; set; }
|
||||
public string ErrorCode { get; set; }
|
||||
|
||||
// Installer result codes
|
||||
public string ExitCode { get; set; }
|
||||
public string HResultFromWin32 { get; set; }
|
||||
|
||||
// Delivery Optimization telemetry
|
||||
public long DoFileSize { get; set; }
|
||||
public long DoTotalBytesDownloaded { get; set; }
|
||||
public long DoBytesFromPeers { get; set; }
|
||||
public int DoPercentPeerCaching { get; set; }
|
||||
public long DoBytesFromLanPeers { get; set; }
|
||||
public long DoBytesFromGroupPeers { get; set; }
|
||||
public long DoBytesFromInternetPeers { get; set; }
|
||||
public int DoDownloadMode { get; set; } = -1;
|
||||
public string DoDownloadDuration { get; set; }
|
||||
public long DoBytesFromHttp { get; set; }
|
||||
public bool HasDoTelemetry { get; set; }
|
||||
|
||||
// App metadata (captured from IME logs for App Dashboard reporting)
|
||||
public string AppVersion { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public int AttemptNumber { get; set; }
|
||||
public string DetectionResult { get; set; }
|
||||
}
|
||||
|
||||
public class FilePositionData
|
||||
{
|
||||
public long Position { get; set; }
|
||||
public long LastKnownSize { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user