diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs index 194b82ce..5f7b9664 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs @@ -283,7 +283,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo EmitDeviceInfoEvent("autopilot_profile", "Autopilot profile configuration", data); // Emit dedicated enrollment_type_detected event for easy filtering - _emitEvent(new EnrollmentEvent + _post.Emit(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, @@ -751,7 +751,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo private void EmitDeviceInfoEvent(string eventType, string message, Dictionary data) { - _emitEvent(new EnrollmentEvent + _post.Emit(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs index 0754b8aa..406b06cf 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs @@ -7,6 +7,7 @@ using System.Net.NetworkInformation; using System.Text.Json; using System.Threading.Tasks; using AutopilotMonitor.Agent.V2.Core.Logging; +using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment; @@ -25,7 +26,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo { private readonly string _sessionId; private readonly string _tenantId; - private readonly Action _emitEvent; + private readonly InformationalEventPost _post; private readonly AgentLogger _logger; private const string RegKeyWindowsCurrentVersion = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion"; @@ -33,12 +34,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo public DeviceInfoCollector( string sessionId, string tenantId, - Action emitEvent, + InformationalEventPost post, AgentLogger logger) { _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId)); _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); - _emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent)); + _post = post ?? throw new ArgumentNullException(nameof(post)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs index 311a21a2..9ef4ecea 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs @@ -126,6 +126,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration hosts.Add(new AadJoinHost(logger, ingress, clock)); + // Single-rail refactor (plan §5.8) — DeviceInfoCollector existed in V2.Core but had + // no host so the Device-Details UI block was empty in V2 sessions (V1-Parity Issue #2). + // Kernel host: always on, not remote-config-gated; fires CollectAll on Start on a + // background thread so the agent's critical path is not blocked by WMI queries. + hosts.Add(new DeviceInfoHost( + sessionId: sessionId, + tenantId: tenantId, + ingress: ingress, + clock: clock, + logger: logger)); + // Dev / test — if --replay-log-dir is set, the tracker reads from the replay folder // with SimulationMode ON + the configured SpeedFactor instead of tailing the live // IME log folder. Production agents leave ReplayLogDir empty. @@ -379,6 +390,75 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration } } + /// + /// Single-rail refactor (plan §5.8) — wraps + /// to deliver the V1 "Device Details" event surface (OS / hardware / TPM / BitLocker / + /// AAD-Join / autopilot profile / ESP config / network / hardware spec, 14 event types). + /// + /// Kernel host (not remote-config-gated). Fires + /// on on a ThreadPool task so the orchestrator's critical path is not + /// blocked by the underlying WMI / registry / networking probes. Exceptions from the task + /// are swallowed and logged; a failure in any one sub-emit must not kill the agent. + /// + /// + /// Out of scope today: the Legacy EnrollmentTracker also called + /// CollectAtEnrollmentStart on first DeviceSetup phase detection (re-fetches AAD + /// join / autopilot profile / ESP config / TPM once MDM enrollment has populated them) + /// and CollectAtEnd at termination (re-fetches BitLocker + active NIC). Hooking + /// those into the V2 signal timeline requires a reducer-driven subscription that does + /// not exist yet. The basic path solves Parity Issue #2 for the + /// dominant case; the phase-transition re-collections are a follow-up (plan §5.8 TODO). + /// + /// + private sealed class DeviceInfoHost : ICollectorHost + { + public string Name => "DeviceInfoCollector"; + + private readonly Monitoring.Telemetry.DeviceInfo.DeviceInfoCollector _collector; + private readonly AgentLogger _logger; + private int _disposed; + + public DeviceInfoHost( + string sessionId, + string tenantId, + ISignalIngressSink ingress, + IClock clock, + AgentLogger logger) + { + if (ingress == null) throw new ArgumentNullException(nameof(ingress)); + if (clock == null) throw new ArgumentNullException(nameof(clock)); + _logger = logger; + var post = new InformationalEventPost(ingress, clock); + _collector = new Monitoring.Telemetry.DeviceInfo.DeviceInfoCollector( + sessionId, tenantId, post, logger); + } + + public void Start() + { + // Fire-and-forget — WMI queries can take several seconds and must not block the + // orchestrator's Start path. The collector emits its 13+ events into the ingress + // pipe as each sub-collector completes. + System.Threading.Tasks.Task.Run(() => + { + try { _collector.CollectAll(); } + catch (Exception ex) { _logger.Warning($"DeviceInfoHost: CollectAll threw: {ex.Message}"); } + }); + _logger.Info("DeviceInfoHost: CollectAll scheduled on background thread."); + } + + public void Stop() + { + // No background worker to stop; the scheduled Task either completed or is still + // running and will exit once it finishes emitting. Stop is a no-op by design. + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) return; + // DeviceInfoCollector does not implement IDisposable — purely event-emitting. + } + } + private sealed class AadJoinHost : ICollectorHost { public string Name => "AadJoinWatcher";