From ddb8daceee4e050e85837bbc0ab7b41fea04cc59 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 17 Mar 2026 19:08:48 +0100 Subject: [PATCH] added self-deploying mode detection and event tracking for the enrollment finalization process. --- .../DeviceInfoCollector.NetworkAndSecurity.cs | 6 +- .../Collectors/DeviceInfoCollector.cs | 4 +- ...HelloTracker.ProvisioningStatusTracking.cs | 22 +++++ .../Collectors/EspAndHelloTracker.cs | 7 ++ .../Tracking/EnrollmentStatePersistence.cs | 4 + .../EnrollmentTracker.CompletionLogic.cs | 90 +++++++++++++++++-- .../Tracking/EnrollmentTracker.Diagnostics.cs | 3 + .../EnrollmentTracker.EventHandlers.cs | 8 ++ .../Monitoring/Tracking/EnrollmentTracker.cs | 21 ++++- .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- 10 files changed, 156 insertions(+), 11 deletions(-) diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs index 9b08da5c..16ea4d3f 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs @@ -131,10 +131,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors /// Note: AutopilotMode and DomainJoinMethod reflect profile capabilities (what is allowed), /// not the actual live enrollment state. /// - private (string enrollmentType, bool isHybridJoin) CollectAutopilotProfile() + private (string enrollmentType, bool isHybridJoin, int? autopilotMode) CollectAutopilotProfile() { var detectedType = "v1"; var isHybridJoin = false; + int? detectedAutopilotMode = null; try { @@ -185,6 +186,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors : null; if (autopilotModeStr != null && int.TryParse(autopilotModeStr, out var autopilotMode)) { + detectedAutopilotMode = autopilotMode; string modeLabel; switch (autopilotMode) { @@ -269,7 +271,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors _logger.Warning($"EnrollmentTracker: failed to collect autopilot profile: {ex.Message}"); } - return (detectedType, isHybridJoin); + return (detectedType, isHybridJoin, detectedAutopilotMode); } private void CollectSecureBootStatus() diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs index 4c5de677..5cb4214d 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs @@ -44,7 +44,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors /// Runs all device info collectors at agent startup. /// Returns the detected enrollment type, Hybrid Join flag, and ESP skip configuration from registry. /// - public (string enrollmentType, bool isHybridJoin, bool? skipUserStatusPage, bool? skipDeviceStatusPage) CollectAll() + public (string enrollmentType, bool isHybridJoin, bool? skipUserStatusPage, bool? skipDeviceStatusPage, int? autopilotMode) CollectAll() { _logger.Info("EnrollmentTracker: collecting device info (at start)"); @@ -59,7 +59,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors CollectAadJoinStatus(); CollectActiveNetworkInterfaceInfo(); - return (profileResult.enrollmentType, profileResult.isHybridJoin, espConfig.skipUserStatusPage, espConfig.skipDeviceStatusPage); + return (profileResult.enrollmentType, profileResult.isHybridJoin, espConfig.skipUserStatusPage, espConfig.skipDeviceStatusPage, profileResult.autopilotMode); } /// diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs index 3e291a1c..b50d5817 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs @@ -55,6 +55,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors private HashSet _provisioningCategoriesResolved; // Track subcategory states per category — detect meaningful state transitions private Dictionary> _lastSubcategoryStates; + // Fire-once guard for DeviceSetupProvisioningComplete event + private bool _deviceSetupProvisioningCompleteFired; private void StartProvisioningStatusWatcher() { @@ -64,6 +66,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors _provisioningFailureFired = new HashSet(StringComparer.OrdinalIgnoreCase); _provisioningCategoriesResolved = new HashSet(StringComparer.OrdinalIgnoreCase); _lastSubcategoryStates = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _deviceSetupProvisioningCompleteFired = false; // Try to start immediately; if key doesn't exist yet, retry every 2s if (!TryStartProvisioningWatcher()) @@ -228,6 +231,25 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors _logger.Error($"EspFailureDetected handler failed for '{failureType}'", ex); } } + + // Fire DeviceSetupProvisioningComplete when DeviceSetup category resolves with success. + // Used as completion signal for Self-Deploying mode where Shell-Core ESP exit + // and desktop arrival may never arrive. + if (!_deviceSetupProvisioningCompleteFired + && _lastCategorySucceeded.TryGetValue("DeviceSetupCategory.Status", out var dsSucceeded) + && dsSucceeded == true) + { + _deviceSetupProvisioningCompleteFired = true; + _logger.Info("Provisioning status: DeviceSetup succeeded — firing DeviceSetupProvisioningComplete"); + try + { + DeviceSetupProvisioningComplete?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _logger.Error("DeviceSetupProvisioningComplete handler failed", ex); + } + } } } } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs index 78d8b7e9..ac98be0c 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs @@ -77,6 +77,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors /// public event EventHandler EspFailureDetected; + /// + /// Fired when DeviceSetup provisioning status shows categorySucceeded=true. + /// Used as a completion signal for Self-Deploying mode where Shell-Core ESP exit + /// and desktop arrival signals may never arrive. + /// + public event EventHandler DeviceSetupProvisioningComplete; + /// /// Outcome of Hello provisioning. Set when Hello resolves (via events, timeout, or not configured). /// Values: "completed", "timeout", "not_configured", "wizard_not_started", null (not yet resolved). diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs index d75ef15f..ac882415 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs @@ -111,12 +111,16 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking public bool? SkipUserStatusPage { get; set; } public bool? SkipDeviceStatusPage { get; set; } + // Autopilot deployment mode: 0=UserDriven, 1=SelfDeploying, 2=PreProvisioning, null=unknown + public int? AutopilotMode { get; set; } + // Signal timestamps for audit trail public DateTime? EspFirstSeenUtc { get; set; } public DateTime? EspFinalExitUtc { get; set; } public DateTime? DesktopArrivedUtc { get; set; } public DateTime? HelloResolvedUtc { get; set; } public DateTime? ImePatternSeenUtc { get; set; } + public DateTime? DeviceSetupProvisioningCompleteUtc { get; set; } public List SignalsSeen { get; set; } = new List(); } } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs index d51a4a67..ba0cb9fd 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs @@ -47,6 +47,74 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking } } + /// + /// Called when DeviceSetup provisioning status shows all subcategories succeeded. + /// In Self-Deploying mode, this is the primary completion signal because no user session + /// means no Hello, no desktop arrival, and possibly no Shell-Core ESP exit event. + /// In non-Self-Deploying mode, this is informational only — normal paths handle completion. + /// + private void OnDeviceSetupProvisioningComplete(object sender, EventArgs e) + { + _logger.Info("EnrollmentTracker: DeviceSetup provisioning completed successfully"); + RecordSignal("device_setup_provisioning_complete"); + _stateData.DeviceSetupProvisioningCompleteUtc = DateTime.UtcNow; + _stateDirty = true; + + if (!IsSelfDeploying) + { + _logger.Info("EnrollmentTracker: DeviceSetup provisioning complete but not Self-Deploying — " + + "using normal completion paths (ESP exit + Hello)"); + EmitTraceEvent("device_setup_complete_non_self_deploying", + "DeviceSetup provisioning complete in non-Self-Deploying mode — no action, normal paths apply", + new Dictionary { { "autopilotMode", _autopilotMode } }); + return; + } + + // Self-Deploying mode: this is our primary completion signal. + // No user session → no Hello, no desktop arrival, possibly no Shell-Core ESP exit. + _logger.Info("EnrollmentTracker: Self-Deploying mode + DeviceSetup provisioning complete — " + + "transitioning to FinalizingSetup and attempting completion"); + + // Mark ESP as seen and final exit (provisioning success implies ESP device phase completed) + if (!_espEverSeen) + { + _espEverSeen = true; + _stateData.EspEverSeen = true; + } + if (!_espFinalExitSeen) + { + _espFinalExitSeen = true; + _stateData.EspFinalExitSeen = true; + _stateData.EspFinalExitUtc = DateTime.UtcNow; + RecordSignal("self_deploying_esp_final_exit"); + } + + // Emit FinalizingSetup phase event + _emitEvent(new EnrollmentEvent + { + SessionId = _sessionId, + TenantId = _tenantId, + EventType = "esp_phase_changed", + Severity = EventSeverity.Info, + Source = "EnrollmentTracker", + Phase = EnrollmentPhase.FinalizingSetup, + Message = "ESP phase: FinalizingSetup (Self-Deploying — DeviceSetup provisioning complete, no user session expected)", + Data = new Dictionary + { + { "espPhase", "FinalizingSetup" }, + { "autoDetected", true }, + { "triggeredBy", "self_deploying_provisioning_complete" }, + { "autopilotMode", _autopilotMode }, + { "skipUserStatusPage", _skipUserStatusPage } + } + }); + + CollectDeviceInfoAtFinalizingSetup("self_deploying_provisioning_complete"); + + // Attempt completion — Hello guard is bypassed for Self-Deploying (IsSelfDeploying) + TryEmitEnrollmentComplete("self_deploying_provisioning_complete"); + } + /// /// Called when WhiteGlove (Pre-Provisioning) completes successfully. /// Emits the whiteglove_complete event. The MonitoringService handles @@ -348,6 +416,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _enrollmentType = loaded.EnrollmentType ?? _enrollmentType; _skipUserStatusPage = loaded.SkipUserStatusPage; _skipDeviceStatusPage = loaded.SkipDeviceStatusPage; + _autopilotMode = loaded.AutopilotMode; _stateData = loaded; _logger.Info($"EnrollmentTracker: state restored — espEverSeen={_espEverSeen}, espFinalExitSeen={_espFinalExitSeen}, desktopArrived={_desktopArrived}, lastEspPhase={_lastEspPhase}, enrollmentCompleteEmitted={_enrollmentCompleteEmitted}"); @@ -410,12 +479,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _logger.Debug($"EnrollmentTracker: TryEmitEnrollmentComplete('{source}') — evaluating guards " + $"[espEverSeen={_espEverSeen}, espFinalExitSeen={_espFinalExitSeen}, desktopArrived={_desktopArrived}, " + $"helloCompleted={_espAndHelloTracker?.IsHelloCompleted ?? false}, helloPolicyConfigured={_espAndHelloTracker?.IsPolicyConfigured ?? false}, " + - $"enrollmentType={_enrollmentType}]"); + $"enrollmentType={_enrollmentType}, autopilotMode={_autopilotMode}, isSelfDeploying={IsSelfDeploying}]"); - // Hello-Check: Hello must be resolved before we can complete + // Hello-Check: Hello must be resolved before we can complete. + // Self-Deploying mode (AutopilotMode=1) has no user session, so Hello is irrelevant — + // bypass the Hello guard entirely regardless of whether WHfB policy is configured. bool helloResolved = _espAndHelloTracker == null || _espAndHelloTracker.IsHelloCompleted - || !_espAndHelloTracker.IsPolicyConfigured; + || !_espAndHelloTracker.IsPolicyConfigured + || IsSelfDeploying; if (!helloResolved) { @@ -458,6 +530,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking signalTimestamps["helloResolved"] = _stateData.HelloResolvedUtc.Value.ToString("o"); if (_stateData.ImePatternSeenUtc.HasValue) signalTimestamps["imePatternSeen"] = _stateData.ImePatternSeenUtc.Value.ToString("o"); + if (_stateData.DeviceSetupProvisioningCompleteUtc.HasValue) + signalTimestamps["deviceSetupProvisioningComplete"] = _stateData.DeviceSetupProvisioningCompleteUtc.Value.ToString("o"); _emitEvent(new EnrollmentEvent { @@ -472,6 +546,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { { "completionSource", source }, { "helloOutcome", helloOutcome }, + { "autopilotMode", _autopilotMode?.ToString() ?? "unknown" }, + { "isSelfDeploying", IsSelfDeploying }, { "signalsSeen", _stateData.SignalsSeen }, { "signalTimestamps", signalTimestamps }, { "agentUptimeSeconds", (DateTime.UtcNow - _agentStartTimeUtc).TotalSeconds } @@ -521,7 +597,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { "enrollmentType", _enrollmentType }, { "lastEspPhase", _lastEspPhase ?? "none" }, { "skipUserStatusPage", _skipUserStatusPage?.ToString() ?? "unknown" }, - { "skipDeviceStatusPage", _skipDeviceStatusPage?.ToString() ?? "unknown" } + { "skipDeviceStatusPage", _skipDeviceStatusPage?.ToString() ?? "unknown" }, + { "autopilotMode", _autopilotMode?.ToString() ?? "unknown" }, + { "isSelfDeploying", IsSelfDeploying } } }); } @@ -615,8 +693,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking // During active ESP (AccountSetup runs in background with desktop visible), // the Hello timer must wait until ESP exits (started in OnFinalizingSetupPhaseTriggered). // Without this guard, Hello timeout-resolves while ESP still installs apps → premature completion. + // Self-Deploying: no user session, Hello is irrelevant — skip timer entirely. if (_espAndHelloTracker != null && !_espAndHelloTracker.IsHelloCompleted - && (!_espEverSeen || _espFinalExitSeen)) + && (!_espEverSeen || _espFinalExitSeen) + && !IsSelfDeploying) { _logger.Info("EnrollmentTracker: Desktop arrived with Hello pending (no active ESP) — starting Hello wait timer"); _espAndHelloTracker.StartHelloWaitTimer(); diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs index fe6e4486..65252c86 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs @@ -167,6 +167,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _espAndHelloTracker.FinalizingSetupPhaseTriggered -= OnFinalizingSetupPhaseTriggered; _espAndHelloTracker.WhiteGloveCompleted -= OnWhiteGloveCompleted; _espAndHelloTracker.EspFailureDetected -= OnEspFailureDetected; + _espAndHelloTracker.DeviceSetupProvisioningComplete -= OnDeviceSetupProvisioningComplete; } _espFailureTimer?.Dispose(); @@ -223,6 +224,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking signalTimestamps["helloResolved"] = _stateData.HelloResolvedUtc.Value.ToString("o"); if (_stateData.ImePatternSeenUtc.HasValue) signalTimestamps["imePatternSeen"] = _stateData.ImePatternSeenUtc.Value.ToString("o"); + if (_stateData.DeviceSetupProvisioningCompleteUtc.HasValue) + signalTimestamps["deviceSetupProvisioningComplete"] = _stateData.DeviceSetupProvisioningCompleteUtc.Value.ToString("o"); // Build per-phase package states: snapshots from completed phases + current phase var phaseSnapshots = _imeLogTracker?.PhasePackageSnapshots; diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs index cff64b3b..4699488c 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs @@ -501,6 +501,14 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _debugStateTimer?.Change(Timeout.Infinite, Timeout.Infinite); _debugStateTimerActive = false; + // Self-Deploying mode: skip Hello wait entirely — no user session for Hello + if (IsSelfDeploying) + { + _logger.Info("EnrollmentTracker: Self-Deploying mode — skipping Hello wait, proceeding to completion"); + TryEmitEnrollmentComplete("ime_pattern"); + return; + } + // Check if Windows Hello is configured but not yet completed if (_espAndHelloTracker != null) { diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs index ca19cf3c..471f3b15 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs @@ -66,6 +66,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking private bool? _skipUserStatusPage; private bool? _skipDeviceStatusPage; + // Autopilot deployment mode: 0=UserDriven, 1=SelfDeploying, 2=PreProvisioning, null=unknown + private int? _autopilotMode; + private bool IsSelfDeploying => _autopilotMode == 1; + // Safety-net timer for waiting_for_hello state private Timer _waitingForHelloSafetyTimer; private const int WaitingForHelloSafetyTimeoutSeconds = 420; // 7 min — longer than Hello chain (330s) @@ -190,6 +194,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _espAndHelloTracker.FinalizingSetupPhaseTriggered += OnFinalizingSetupPhaseTriggered; _espAndHelloTracker.WhiteGloveCompleted += OnWhiteGloveCompleted; _espAndHelloTracker.EspFailureDetected += OnEspFailureDetected; + _espAndHelloTracker.DeviceSetupProvisioningComplete += OnDeviceSetupProvisioningComplete; } } @@ -298,13 +303,27 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking _enrollmentType = result.enrollmentType; _skipUserStatusPage = result.skipUserStatusPage; _skipDeviceStatusPage = result.skipDeviceStatusPage; + _autopilotMode = result.autopilotMode; _stateData.EnrollmentType = _enrollmentType; _stateData.SkipUserStatusPage = _skipUserStatusPage; _stateData.SkipDeviceStatusPage = _skipDeviceStatusPage; + _stateData.AutopilotMode = _autopilotMode; _stateDirty = true; _logger.Info($"EnrollmentTracker: enrollment type detected: {_enrollmentType ?? "unknown"} " + - $"(skipUserStatusPage={_skipUserStatusPage}, skipDeviceStatusPage={_skipDeviceStatusPage})"); + $"(autopilotMode={_autopilotMode}, skipUserStatusPage={_skipUserStatusPage}, skipDeviceStatusPage={_skipDeviceStatusPage})"); + + if (IsSelfDeploying) + { + EmitTraceEvent("self_deploying_detected", + "AutopilotMode=1 (Self-Deploying) — Hello guard will be bypassed, provisioning status used as completion signal", + new Dictionary + { + { "autopilotMode", _autopilotMode }, + { "skipUserStatusPage", _skipUserStatusPage }, + { "enrollmentType", _enrollmentType } + }); + } } /// diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index bf7aeeb3..26551cf1 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -462 +463