diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/AnalyzerManager.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/AnalyzerManager.cs
new file mode 100644
index 00000000..953cafb1
--- /dev/null
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/AnalyzerManager.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AutopilotMonitor.Agent.Core.Configuration;
+using AutopilotMonitor.Agent.Core.Logging;
+using AutopilotMonitor.Agent.Core.Monitoring.Analyzers;
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Agent.Core.Monitoring.Core
+{
+ ///
+ /// Manages agent analyzers: initialization from remote config, startup execution
+ /// on a background thread, and shutdown execution with optional WhiteGlove part.
+ ///
+ public class AnalyzerManager
+ {
+ private readonly AgentConfiguration _configuration;
+ private readonly AgentLogger _logger;
+ private readonly Action _emitEvent;
+ private readonly RemoteConfigService _remoteConfigService;
+
+ private readonly List _analyzers = new List();
+
+ public AnalyzerManager(
+ AgentConfiguration configuration,
+ AgentLogger logger,
+ Action emitEvent,
+ RemoteConfigService remoteConfigService)
+ {
+ _configuration = configuration;
+ _logger = logger;
+ _emitEvent = emitEvent;
+ _remoteConfigService = remoteConfigService;
+ }
+
+ public void Initialize()
+ {
+ _analyzers.Clear();
+
+ var analyzerConfig = _remoteConfigService?.CurrentConfig?.Analyzers ?? new AnalyzerConfiguration();
+
+ if (analyzerConfig.EnableLocalAdminAnalyzer)
+ {
+ _analyzers.Add(new LocalAdminAnalyzer(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ _emitEvent,
+ _logger,
+ analyzerConfig.LocalAdminAllowedAccounts
+ ));
+ _logger.Info("LocalAdminAnalyzer registered");
+ }
+ else
+ {
+ _logger.Info("LocalAdminAnalyzer disabled by remote config");
+ }
+
+ if (analyzerConfig.EnableSoftwareInventoryAnalyzer)
+ {
+ _analyzers.Add(new SoftwareInventoryAnalyzer(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ _emitEvent,
+ _logger
+ ));
+ _logger.Info("SoftwareInventoryAnalyzer registered");
+ }
+ else
+ {
+ _logger.Info("SoftwareInventoryAnalyzer disabled by remote config");
+ }
+
+ _logger.Info($"Analyzers initialized: {_analyzers.Count} active");
+ }
+
+ public void RunStartup()
+ {
+ if (_analyzers.Count == 0)
+ return;
+
+ var analyzers = new List(_analyzers);
+
+ _logger.Info($"Scheduling {analyzers.Count} startup analyzer(s) on background thread");
+
+ Task.Run(() =>
+ {
+ foreach (var analyzer in analyzers)
+ {
+ try
+ {
+ analyzer.AnalyzeAtStartup();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Analyzer {analyzer.Name} threw during startup", ex);
+ }
+ }
+ });
+ }
+
+ public void RunShutdown(int? whiteGlovePart = null)
+ {
+ if (_analyzers.Count == 0)
+ return;
+
+ _logger.Info($"Running {_analyzers.Count} shutdown analyzer(s) (whiteGlovePart={whiteGlovePart?.ToString() ?? "none"})");
+
+ foreach (var analyzer in _analyzers)
+ {
+ try
+ {
+ if (analyzer is SoftwareInventoryAnalyzer softwareAnalyzer)
+ softwareAnalyzer.AnalyzeAtShutdown(whiteGlovePart);
+ else
+ analyzer.AnalyzeAtShutdown();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Analyzer {analyzer.Name} threw during shutdown", ex);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CollectorCoordinator.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CollectorCoordinator.cs
index af39005f..5127d232 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CollectorCoordinator.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/CollectorCoordinator.cs
@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutopilotMonitor.Agent.Core.Configuration;
using AutopilotMonitor.Agent.Core.Logging;
-using AutopilotMonitor.Agent.Core.Monitoring.Analyzers;
using AutopilotMonitor.Agent.Core.Monitoring.Collectors;
using AutopilotMonitor.Agent.Core.Monitoring.Network;
using AutopilotMonitor.Agent.Core.Monitoring.Replay;
@@ -39,11 +37,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
private LogReplayService _logReplay;
// Optional collectors (toggled via remote config)
- private PerformanceCollector _performanceCollector;
- private AgentSelfMetricsCollector _agentSelfMetricsCollector;
private DeliveryOptimizationCollector _deliveryOptimizationCollector;
private StallProbeCollector _stallProbeCollector;
+ // Periodic collectors + idle management
+ private PeriodicCollectorManager _periodicManager;
+
// Smart enrollment tracking
private EnrollmentTracker _enrollmentTracker;
@@ -56,13 +55,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
private NetworkChangeDetector _networkChangeDetector;
// Analyzers
- private readonly List _analyzers = new List();
-
- // Idle detection
- private DateTime _lastRealEventTime = DateTime.UtcNow;
- private bool _collectorsIdleStopped;
- private int _collectorIdleTimeoutMinutes = 15;
- private Timer _idleCheckTimer;
+ private AnalyzerManager _analyzerManager;
// Max lifetime safety net
private Timer _maxLifetimeTimer;
@@ -98,7 +91,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
///
/// Whether periodic collectors are currently paused due to idle timeout.
///
- public bool CollectorsIdleStopped => _collectorsIdleStopped;
+ public bool CollectorsIdleStopped => _periodicManager?.CollectorsIdleStopped ?? false;
///
/// Called from EmitEvent when a non-periodic event is received.
@@ -106,15 +99,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
///
public void OnRealEventReceived()
{
- _lastRealEventTime = DateTime.UtcNow;
-
- try { _stallProbeCollector?.ResetProbes(); }
- catch (Exception ex) { _logger.Verbose($"StallProbeCollector.ResetProbes failed: {ex.Message}"); }
-
- if (_collectorsIdleStopped)
- {
- RestartPeriodicCollectors();
- }
+ _periodicManager?.OnRealEventReceived();
}
///
@@ -148,9 +133,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
///
public void StopPerformanceCollectorOnly()
{
- _performanceCollector?.Stop();
- _performanceCollector?.Dispose();
- _performanceCollector = null;
+ _periodicManager?.StopPerformanceCollectorOnly();
}
///
@@ -166,13 +149,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
///
public static bool IsPeriodicEvent(string eventType)
{
- return eventType == "performance_snapshot" ||
- eventType == "agent_metrics_snapshot" ||
- eventType == "performance_collector_stopped" ||
- eventType == "agent_metrics_collector_stopped" ||
- eventType == Constants.EventTypes.StallProbeCheck ||
- eventType == Constants.EventTypes.StallProbeResult ||
- eventType == Constants.EventTypes.SessionStalled;
+ return PeriodicCollectorManager.IsPeriodicEvent(eventType);
}
public void StartEventCollectors()
@@ -235,37 +212,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
try
{
- var perfInterval = collectors?.PerformanceIntervalSeconds ?? 60;
- _performanceCollector = new PerformanceCollector(
- _configuration.SessionId,
- _configuration.TenantId,
- _emitEvent,
- _logger,
- perfInterval
- );
- _performanceCollector.Start();
- _logger.Info($"PerformanceCollector started (interval={perfInterval}s)");
-
- if (collectors?.EnableAgentSelfMetrics != false)
- {
- var selfMetricsInterval = collectors?.AgentSelfMetricsIntervalSeconds ?? 60;
- _agentSelfMetricsCollector = new AgentSelfMetricsCollector(
- _configuration.SessionId,
- _configuration.TenantId,
- _emitEvent,
- _apiClient.NetworkMetrics,
- _spool,
- _logger,
- _agentVersion,
- selfMetricsInterval
- );
- _agentSelfMetricsCollector.Start();
- _logger.Info($"AgentSelfMetricsCollector started (interval={selfMetricsInterval}s)");
- }
-
- _collectorIdleTimeoutMinutes = collectors?.CollectorIdleTimeoutMinutes ?? 15;
- _lastRealEventTime = DateTime.UtcNow;
- _collectorsIdleStopped = false;
+ _periodicManager = new PeriodicCollectorManager(
+ _configuration, _logger, _agentVersion,
+ _emitEvent, _apiClient, _spool, _remoteConfigService);
+ _periodicManager.Start();
if (collectors?.StallProbeEnabled != false)
{
@@ -292,16 +242,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_logger.Info("StallProbeCollector disabled via config");
}
- _idleCheckTimer = new Timer(
- IdleCheckCallback,
- null,
- TimeSpan.FromSeconds(60),
- TimeSpan.FromSeconds(60)
- );
- if (_collectorIdleTimeoutMinutes > 0)
- _logger.Info($"Collector idle timeout enabled: {_collectorIdleTimeoutMinutes} min");
- else
- _logger.Info("Collector idle timeout disabled (0)");
+ _periodicManager.StallProbeCollector = _stallProbeCollector;
if (string.IsNullOrEmpty(_configuration.ReplayLogDir))
{
@@ -421,8 +362,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
public void StopOptionalCollectors()
{
- _idleCheckTimer?.Dispose();
- _idleCheckTimer = null;
+ _periodicManager?.Stop();
_maxLifetimeTimer?.Dispose();
_maxLifetimeTimer = null;
@@ -440,14 +380,6 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_networkChangeDetector?.Dispose();
_networkChangeDetector = null;
- _performanceCollector?.Stop();
- _performanceCollector?.Dispose();
- _performanceCollector = null;
-
- _agentSelfMetricsCollector?.Stop();
- _agentSelfMetricsCollector?.Dispose();
- _agentSelfMetricsCollector = null;
-
_deliveryOptimizationCollector?.Stop();
_deliveryOptimizationCollector?.Dispose();
_deliveryOptimizationCollector = null;
@@ -512,90 +444,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
public void InitializeAnalyzers()
{
- _analyzers.Clear();
-
- var analyzerConfig = _remoteConfigService?.CurrentConfig?.Analyzers ?? new AnalyzerConfiguration();
-
- if (analyzerConfig.EnableLocalAdminAnalyzer)
- {
- _analyzers.Add(new LocalAdminAnalyzer(
- _configuration.SessionId,
- _configuration.TenantId,
- _emitEvent,
- _logger,
- analyzerConfig.LocalAdminAllowedAccounts
- ));
- _logger.Info("LocalAdminAnalyzer registered");
- }
- else
- {
- _logger.Info("LocalAdminAnalyzer disabled by remote config");
- }
-
- if (analyzerConfig.EnableSoftwareInventoryAnalyzer)
- {
- _analyzers.Add(new SoftwareInventoryAnalyzer(
- _configuration.SessionId,
- _configuration.TenantId,
- _emitEvent,
- _logger
- ));
- _logger.Info("SoftwareInventoryAnalyzer registered");
- }
- else
- {
- _logger.Info("SoftwareInventoryAnalyzer disabled by remote config");
- }
-
- _logger.Info($"Analyzers initialized: {_analyzers.Count} active");
+ _analyzerManager = new AnalyzerManager(_configuration, _logger, _emitEvent, _remoteConfigService);
+ _analyzerManager.Initialize();
}
public void RunStartupAnalyzers()
{
- if (_analyzers.Count == 0)
- return;
-
- var analyzers = new List(_analyzers);
-
- _logger.Info($"Scheduling {analyzers.Count} startup analyzer(s) on background thread");
-
- Task.Run(() =>
- {
- foreach (var analyzer in analyzers)
- {
- try
- {
- analyzer.AnalyzeAtStartup();
- }
- catch (Exception ex)
- {
- _logger.Error($"Analyzer {analyzer.Name} threw during startup", ex);
- }
- }
- });
+ _analyzerManager?.RunStartup();
}
public void RunShutdownAnalyzers(int? whiteGlovePart = null)
{
- if (_analyzers.Count == 0)
- return;
-
- _logger.Info($"Running {_analyzers.Count} shutdown analyzer(s) (whiteGlovePart={whiteGlovePart?.ToString() ?? "none"})");
-
- foreach (var analyzer in _analyzers)
- {
- try
- {
- if (analyzer is SoftwareInventoryAnalyzer softwareAnalyzer)
- softwareAnalyzer.AnalyzeAtShutdown(whiteGlovePart);
- else
- analyzer.AnalyzeAtShutdown();
- }
- catch (Exception ex)
- {
- _logger.Error($"Analyzer {analyzer.Name} threw during shutdown", ex);
- }
- }
+ _analyzerManager?.RunShutdown(whiteGlovePart);
}
///
@@ -659,138 +519,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_enrollmentTracker?.NotifyDesktopArrived();
}
- private void IdleCheckCallback(object state)
- {
- var idleMinutes = (DateTime.UtcNow - _lastRealEventTime).TotalMinutes;
-
- try
- {
- _stallProbeCollector?.CheckAndRunProbes(idleMinutes);
- }
- catch (Exception ex)
- {
- _logger.Error("StallProbeCollector.CheckAndRunProbes threw", ex);
- }
-
- if (_collectorsIdleStopped)
- return;
-
- if (idleMinutes < _collectorIdleTimeoutMinutes)
- return;
-
- _logger.Info($"Collector idle timeout reached ({_collectorIdleTimeoutMinutes} min, idle for {idleMinutes:F0} min) — stopping periodic collectors");
-
- if (_performanceCollector != null)
- {
- _performanceCollector.Stop();
- _performanceCollector.Dispose();
- _performanceCollector = null;
-
- _emitEvent(new EnrollmentEvent
- {
- SessionId = _configuration.SessionId,
- TenantId = _configuration.TenantId,
- Timestamp = DateTime.UtcNow,
- EventType = "performance_collector_stopped",
- Severity = EventSeverity.Info,
- Source = "MonitoringService",
- Phase = EnrollmentPhase.Unknown,
- Message = $"Performance collector stopped after {_collectorIdleTimeoutMinutes} min idle (no real enrollment activity)",
- Data = new Dictionary
- {
- { "reason", "idle_timeout" },
- { "idleTimeoutMinutes", _collectorIdleTimeoutMinutes },
- { "idleMinutes", Math.Round(idleMinutes, 1) }
- }
- });
- }
-
- if (_agentSelfMetricsCollector != null)
- {
- _agentSelfMetricsCollector.Stop();
- _agentSelfMetricsCollector.Dispose();
- _agentSelfMetricsCollector = null;
-
- _emitEvent(new EnrollmentEvent
- {
- SessionId = _configuration.SessionId,
- TenantId = _configuration.TenantId,
- Timestamp = DateTime.UtcNow,
- EventType = "agent_metrics_collector_stopped",
- Severity = EventSeverity.Info,
- Source = "MonitoringService",
- Phase = EnrollmentPhase.Unknown,
- Message = $"AgentSelfMetrics collector stopped after {_collectorIdleTimeoutMinutes} min idle (no real enrollment activity)",
- Data = new Dictionary
- {
- { "reason", "idle_timeout" },
- { "idleTimeoutMinutes", _collectorIdleTimeoutMinutes },
- { "idleMinutes", Math.Round(idleMinutes, 1) }
- }
- });
- }
-
- _collectorsIdleStopped = true;
- }
-
- private void RestartPeriodicCollectors()
- {
- var config = _remoteConfigService?.CurrentConfig;
- var collectors = config?.Collectors;
-
- _logger.Info("Restarting periodic collectors — new enrollment activity detected after idle stop");
-
- try
- {
- var perfInterval = collectors?.PerformanceIntervalSeconds ?? 60;
- _performanceCollector = new PerformanceCollector(
- _configuration.SessionId,
- _configuration.TenantId,
- _emitEvent,
- _logger,
- perfInterval
- );
- _performanceCollector.Start();
-
- if (collectors?.EnableAgentSelfMetrics != false)
- {
- var selfMetricsInterval = collectors?.AgentSelfMetricsIntervalSeconds ?? 60;
- _agentSelfMetricsCollector = new AgentSelfMetricsCollector(
- _configuration.SessionId,
- _configuration.TenantId,
- _emitEvent,
- _apiClient.NetworkMetrics,
- _spool,
- _logger,
- _agentVersion,
- selfMetricsInterval
- );
- _agentSelfMetricsCollector.Start();
- }
-
- _lastRealEventTime = DateTime.UtcNow;
- _collectorsIdleStopped = false;
- if (_collectorIdleTimeoutMinutes > 0)
- {
- _idleCheckTimer?.Change(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
- }
-
- _logger.Info("Periodic collectors restarted successfully");
- }
- catch (Exception ex)
- {
- _logger.Error("Error restarting periodic collectors", ex);
- }
- }
-
public void Dispose()
{
- _idleCheckTimer?.Dispose();
+ _periodicManager?.Dispose();
_maxLifetimeTimer?.Dispose();
_espAndHelloTracker?.Dispose();
_logReplay?.Dispose();
- _performanceCollector?.Dispose();
- _agentSelfMetricsCollector?.Dispose();
_deliveryOptimizationCollector?.Dispose();
_enrollmentTracker?.Dispose();
_networkChangeDetector?.Dispose();
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/PeriodicCollectorManager.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/PeriodicCollectorManager.cs
new file mode 100644
index 00000000..b2537182
--- /dev/null
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/PeriodicCollectorManager.cs
@@ -0,0 +1,306 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using AutopilotMonitor.Agent.Core.Configuration;
+using AutopilotMonitor.Agent.Core.Logging;
+using AutopilotMonitor.Agent.Core.Monitoring.Collectors;
+using AutopilotMonitor.Agent.Core.Monitoring.Network;
+using AutopilotMonitor.Shared;
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Agent.Core.Monitoring.Core
+{
+ ///
+ /// Manages periodic collectors (PerformanceCollector, AgentSelfMetricsCollector) and their
+ /// idle lifecycle: stops them after inactivity, restarts when real enrollment activity resumes.
+ ///
+ public class PeriodicCollectorManager : IDisposable
+ {
+ private readonly AgentConfiguration _configuration;
+ private readonly AgentLogger _logger;
+ private readonly string _agentVersion;
+ private readonly Action _emitEvent;
+ private readonly BackendApiClient _apiClient;
+ private readonly EventSpool _spool;
+ private readonly RemoteConfigService _remoteConfigService;
+
+ private PerformanceCollector _performanceCollector;
+ private AgentSelfMetricsCollector _agentSelfMetricsCollector;
+
+ private DateTime _lastRealEventTime = DateTime.UtcNow;
+ private bool _collectorsIdleStopped;
+ private int _collectorIdleTimeoutMinutes = 15;
+ private Timer _idleCheckTimer;
+
+ ///
+ /// Optional reference to the StallProbeCollector for resetting/running probes
+ /// during idle checks. Set after creation in StartOptionalCollectors.
+ ///
+ public StallProbeCollector StallProbeCollector { get; set; }
+
+ public PeriodicCollectorManager(
+ AgentConfiguration configuration,
+ AgentLogger logger,
+ string agentVersion,
+ Action emitEvent,
+ BackendApiClient apiClient,
+ EventSpool spool,
+ RemoteConfigService remoteConfigService)
+ {
+ _configuration = configuration;
+ _logger = logger;
+ _agentVersion = agentVersion;
+ _emitEvent = emitEvent;
+ _apiClient = apiClient;
+ _spool = spool;
+ _remoteConfigService = remoteConfigService;
+ }
+
+ ///
+ /// Whether periodic collectors are currently paused due to idle timeout.
+ ///
+ public bool CollectorsIdleStopped => _collectorsIdleStopped;
+
+ ///
+ /// Starts the performance and metrics collectors and arms the idle check timer.
+ /// Called from CollectorCoordinator.StartOptionalCollectors.
+ ///
+ public void Start()
+ {
+ var config = _remoteConfigService?.CurrentConfig;
+ var collectors = config?.Collectors;
+
+ var perfInterval = collectors?.PerformanceIntervalSeconds ?? 60;
+ _performanceCollector = new PerformanceCollector(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ _emitEvent,
+ _logger,
+ perfInterval
+ );
+ _performanceCollector.Start();
+ _logger.Info($"PerformanceCollector started (interval={perfInterval}s)");
+
+ if (collectors?.EnableAgentSelfMetrics != false)
+ {
+ var selfMetricsInterval = collectors?.AgentSelfMetricsIntervalSeconds ?? 60;
+ _agentSelfMetricsCollector = new AgentSelfMetricsCollector(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ _emitEvent,
+ _apiClient.NetworkMetrics,
+ _spool,
+ _logger,
+ _agentVersion,
+ selfMetricsInterval
+ );
+ _agentSelfMetricsCollector.Start();
+ _logger.Info($"AgentSelfMetricsCollector started (interval={selfMetricsInterval}s)");
+ }
+
+ _collectorIdleTimeoutMinutes = collectors?.CollectorIdleTimeoutMinutes ?? 15;
+ _lastRealEventTime = DateTime.UtcNow;
+ _collectorsIdleStopped = false;
+
+ _idleCheckTimer = new Timer(
+ IdleCheckCallback,
+ null,
+ TimeSpan.FromSeconds(60),
+ TimeSpan.FromSeconds(60)
+ );
+ if (_collectorIdleTimeoutMinutes > 0)
+ _logger.Info($"Collector idle timeout enabled: {_collectorIdleTimeoutMinutes} min");
+ else
+ _logger.Info("Collector idle timeout disabled (0)");
+ }
+
+ ///
+ /// Called when a non-periodic event is received. Updates idle tracking,
+ /// resets stall probes, and restarts paused collectors.
+ ///
+ public void OnRealEventReceived()
+ {
+ _lastRealEventTime = DateTime.UtcNow;
+
+ try { StallProbeCollector?.ResetProbes(); }
+ catch (Exception ex) { _logger.Verbose($"StallProbeCollector.ResetProbes failed: {ex.Message}"); }
+
+ if (_collectorsIdleStopped)
+ {
+ RestartPeriodicCollectors();
+ }
+ }
+
+ ///
+ /// Stops just the performance collector (used during WhiteGlove completion).
+ ///
+ public void StopPerformanceCollectorOnly()
+ {
+ _performanceCollector?.Stop();
+ _performanceCollector?.Dispose();
+ _performanceCollector = null;
+ }
+
+ ///
+ /// Stops all periodic collectors and the idle check timer.
+ ///
+ public void Stop()
+ {
+ _idleCheckTimer?.Dispose();
+ _idleCheckTimer = null;
+
+ _performanceCollector?.Stop();
+ _performanceCollector?.Dispose();
+ _performanceCollector = null;
+
+ _agentSelfMetricsCollector?.Stop();
+ _agentSelfMetricsCollector?.Dispose();
+ _agentSelfMetricsCollector = null;
+ }
+
+ ///
+ /// Returns true for events generated by periodic timers (not real enrollment activity).
+ ///
+ public static bool IsPeriodicEvent(string eventType)
+ {
+ return eventType == "performance_snapshot" ||
+ eventType == "agent_metrics_snapshot" ||
+ eventType == "performance_collector_stopped" ||
+ eventType == "agent_metrics_collector_stopped" ||
+ eventType == Constants.EventTypes.StallProbeCheck ||
+ eventType == Constants.EventTypes.StallProbeResult ||
+ eventType == Constants.EventTypes.SessionStalled;
+ }
+
+ private void IdleCheckCallback(object state)
+ {
+ var idleMinutes = (DateTime.UtcNow - _lastRealEventTime).TotalMinutes;
+
+ try
+ {
+ StallProbeCollector?.CheckAndRunProbes(idleMinutes);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("StallProbeCollector.CheckAndRunProbes threw", ex);
+ }
+
+ if (_collectorsIdleStopped)
+ return;
+
+ if (idleMinutes < _collectorIdleTimeoutMinutes)
+ return;
+
+ _logger.Info($"Collector idle timeout reached ({_collectorIdleTimeoutMinutes} min, idle for {idleMinutes:F0} min) — stopping periodic collectors");
+
+ if (_performanceCollector != null)
+ {
+ _performanceCollector.Stop();
+ _performanceCollector.Dispose();
+ _performanceCollector = null;
+
+ _emitEvent(new EnrollmentEvent
+ {
+ SessionId = _configuration.SessionId,
+ TenantId = _configuration.TenantId,
+ Timestamp = DateTime.UtcNow,
+ EventType = "performance_collector_stopped",
+ Severity = EventSeverity.Info,
+ Source = "MonitoringService",
+ Phase = EnrollmentPhase.Unknown,
+ Message = $"Performance collector stopped after {_collectorIdleTimeoutMinutes} min idle (no real enrollment activity)",
+ Data = new Dictionary
+ {
+ { "reason", "idle_timeout" },
+ { "idleTimeoutMinutes", _collectorIdleTimeoutMinutes },
+ { "idleMinutes", Math.Round(idleMinutes, 1) }
+ }
+ });
+ }
+
+ if (_agentSelfMetricsCollector != null)
+ {
+ _agentSelfMetricsCollector.Stop();
+ _agentSelfMetricsCollector.Dispose();
+ _agentSelfMetricsCollector = null;
+
+ _emitEvent(new EnrollmentEvent
+ {
+ SessionId = _configuration.SessionId,
+ TenantId = _configuration.TenantId,
+ Timestamp = DateTime.UtcNow,
+ EventType = "agent_metrics_collector_stopped",
+ Severity = EventSeverity.Info,
+ Source = "MonitoringService",
+ Phase = EnrollmentPhase.Unknown,
+ Message = $"AgentSelfMetrics collector stopped after {_collectorIdleTimeoutMinutes} min idle (no real enrollment activity)",
+ Data = new Dictionary
+ {
+ { "reason", "idle_timeout" },
+ { "idleTimeoutMinutes", _collectorIdleTimeoutMinutes },
+ { "idleMinutes", Math.Round(idleMinutes, 1) }
+ }
+ });
+ }
+
+ _collectorsIdleStopped = true;
+ }
+
+ private void RestartPeriodicCollectors()
+ {
+ var config = _remoteConfigService?.CurrentConfig;
+ var collectors = config?.Collectors;
+
+ _logger.Info("Restarting periodic collectors — new enrollment activity detected after idle stop");
+
+ try
+ {
+ var perfInterval = collectors?.PerformanceIntervalSeconds ?? 60;
+ _performanceCollector = new PerformanceCollector(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ _emitEvent,
+ _logger,
+ perfInterval
+ );
+ _performanceCollector.Start();
+
+ if (collectors?.EnableAgentSelfMetrics != false)
+ {
+ var selfMetricsInterval = collectors?.AgentSelfMetricsIntervalSeconds ?? 60;
+ _agentSelfMetricsCollector = new AgentSelfMetricsCollector(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ _emitEvent,
+ _apiClient.NetworkMetrics,
+ _spool,
+ _logger,
+ _agentVersion,
+ selfMetricsInterval
+ );
+ _agentSelfMetricsCollector.Start();
+ }
+
+ _lastRealEventTime = DateTime.UtcNow;
+ _collectorsIdleStopped = false;
+ if (_collectorIdleTimeoutMinutes > 0)
+ {
+ _idleCheckTimer?.Change(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
+ }
+
+ _logger.Info("Periodic collectors restarted successfully");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Error restarting periodic collectors", ex);
+ }
+ }
+
+ public void Dispose()
+ {
+ _idleCheckTimer?.Dispose();
+ _performanceCollector?.Dispose();
+ _agentSelfMetricsCollector?.Dispose();
+ }
+ }
+}
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index 3b2be154..2ebd3a0c 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-941
+945