mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 21:19:38 +00:00
added self-deploying mode detection and event tracking for the enrollment finalization process.
This commit is contained in:
+4
-2
@@ -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.
|
||||
/// </summary>
|
||||
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()
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+22
@@ -55,6 +55,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
private HashSet<string> _provisioningCategoriesResolved;
|
||||
// Track subcategory states per category — detect meaningful state transitions
|
||||
private Dictionary<string, Dictionary<string, string>> _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<string>(StringComparer.OrdinalIgnoreCase);
|
||||
_provisioningCategoriesResolved = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
_lastSubcategoryStates = new Dictionary<string, Dictionary<string, string>>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
/// </summary>
|
||||
public event EventHandler<string> EspFailureDetected;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public event EventHandler DeviceSetupProvisioningComplete;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
|
||||
+4
@@ -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<string> SignalsSeen { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
+85
-5
@@ -47,6 +47,74 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string, object> { { "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<string, object>
|
||||
{
|
||||
{ "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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
+8
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<string, object>
|
||||
{
|
||||
{ "autopilotMode", _autopilotMode },
|
||||
{ "skipUserStatusPage", _skipUserStatusPage },
|
||||
{ "enrollmentType", _enrollmentType }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1 +1 @@
|
||||
462
|
||||
463
|
||||
|
||||
Reference in New Issue
Block a user