mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
Provider-stall tripwire counts device-preparation providers only while device preparation is open (D-383)
A ConfigMgr client installed as a Win32 app registers its device-preparation policy provider after its own install, with InstallationState=1, and nothing completes it. The tripwire then reported a stall long after device preparation had ended. - EspPhaseSnapshot.DevicePreparationOpen decides whether device preparation can still hold the ESP (it runs or failed, or the phase is unknown). EvaluateStallTransition takes it as a parameter; the detector reads the phase through an injectable reader. - The cross-restart fingerprint applies the same filter, so a restart does not report the same stall again. - The message names the wait: Setup/Apps providers hold the ESP without any timeout, device-preparation providers until their timeout. - The diagnosis names a stalled provider only for a failure in its ESP category; the waiting list of an unfinished session stays unfiltered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+150
-2
@@ -185,6 +185,46 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals
|
||||
Assert.False(EspPolicyProviderSnapshot.Empty.SidecarRegistered);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- phase gate for DevicePreparation providers
|
||||
|
||||
[Theory]
|
||||
[InlineData("device_preparation", true)]
|
||||
[InlineData("device_preparation_failed", true)] // a failed device preparation keeps its culprit
|
||||
[InlineData("none", true)] // unknown phase → keep observing
|
||||
[InlineData("unavailable", true)]
|
||||
[InlineData("", true)]
|
||||
[InlineData("device_preparation_done", false)]
|
||||
[InlineData("device_setup", false)]
|
||||
[InlineData("device_setup_failed", false)]
|
||||
[InlineData("account_setup", false)]
|
||||
[InlineData("esp_done", false)]
|
||||
public void Device_preparation_counts_until_it_is_done(string phase, bool expected)
|
||||
=> Assert.Equal(expected, EspPhaseSnapshot.DevicePreparationOpen(phase));
|
||||
|
||||
[Fact]
|
||||
public void DevicePreparation_provider_past_its_phase_drops_out_while_setupApps_still_counts()
|
||||
{
|
||||
// Field shape (client installed as a Win32 app): the client registers its
|
||||
// DevicePreparation provider after its own install with InstallationState=1, long
|
||||
// after device preparation ended. Nothing ever completes it.
|
||||
var snapshot = Snapshot(
|
||||
DevicePrep("ConfigMgr", installState: 1),
|
||||
SetupApps("Contoso", tracking: 0));
|
||||
var seen = new Dictionary<string, DateTime>
|
||||
{
|
||||
["devicePreparation|device|ConfigMgr"] = T0,
|
||||
["setupApps|device|Contoso"] = T0,
|
||||
};
|
||||
|
||||
var eval = EspPolicyProviderStallDetector.EvaluateStallTransition(
|
||||
snapshot, seen, new HashSet<string>(), null, false, T0.AddMinutes(30), Dwell,
|
||||
devicePreparationOpen: false);
|
||||
|
||||
Assert.False(eval.UpdatedFirstSeenUtc.ContainsKey("devicePreparation|device|ConfigMgr")); // reset
|
||||
var stalled = Assert.Single(eval.NewlyStalled);
|
||||
Assert.Equal("Contoso", stalled.Provider.Name);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- arm 2: sidecar-missing transition
|
||||
|
||||
[Fact]
|
||||
@@ -265,17 +305,27 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals
|
||||
|
||||
public void Dispose() => _tmp.Dispose();
|
||||
|
||||
// The phase defaults to "none" so the tests never read the test machine's own ESP state.
|
||||
private (EspPolicyProviderStallDetector sut, FakeSignalIngressSink sink, VirtualClock clock)
|
||||
BuildDetector(StartupEventGate? gate = null)
|
||||
BuildDetector(StartupEventGate? gate = null, Func<string>? phase = null)
|
||||
{
|
||||
var logger = new AgentLogger(_tmp.Path, AgentLogLevel.Info);
|
||||
var sink = new FakeSignalIngressSink();
|
||||
var clock = new VirtualClock(At);
|
||||
var post = new InformationalEventPost(sink, clock);
|
||||
var sut = new EspPolicyProviderStallDetector("S1", "T1", post, logger, clock, gate);
|
||||
var sut = new EspPolicyProviderStallDetector("S1", "T1", post, logger, clock, gate,
|
||||
espPhaseReader: phase ?? (() => EspPhaseSnapshot.None));
|
||||
return (sut, sink, clock);
|
||||
}
|
||||
|
||||
private static readonly TimeSpan DwellSpan = TimeSpan.FromMinutes(EspPolicyProviderStallDetector.DwellMinutes);
|
||||
|
||||
private static PolicyProviderState SidecarDone() =>
|
||||
new PolicyProviderState("Sidecar", "device", EspPolicyProviderProbe.KindSetupApps, 1, null);
|
||||
|
||||
private static PolicyProviderState ConfigMgrDevicePrep(int? installationState) =>
|
||||
new PolicyProviderState("ConfigMgr", "device", EspPolicyProviderProbe.KindDevicePreparation, null, installationState);
|
||||
|
||||
private static EspPolicyProviderSnapshot ConfigMgrOnly() => new EspPolicyProviderSnapshot(new[]
|
||||
{
|
||||
new PolicyProviderState("ConfigMgr", "device", EspPolicyProviderProbe.KindSetupApps,
|
||||
@@ -435,6 +485,104 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Provider_registered_after_device_preparation_never_emits()
|
||||
{
|
||||
// The ConfigMgr client as a Win32 app: its DevicePreparation provider shows up during
|
||||
// device setup with InstallationState=1 and stays there.
|
||||
var snapshot = new EspPolicyProviderSnapshot(new[] { SidecarDone(), ConfigMgrDevicePrep(1) });
|
||||
using var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => snapshot);
|
||||
var (sut, sink, clock) = BuildDetector(phase: () => "device_setup");
|
||||
|
||||
sut.Tick();
|
||||
clock.Advance(TimeSpan.FromHours(2));
|
||||
sut.Tick();
|
||||
|
||||
Assert.Empty(StalledEvents(sink));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_real_device_preparation_hang_still_emits()
|
||||
{
|
||||
// Field shape: only ConfigMgr providers registered, the ESP never left device
|
||||
// preparation and failed with 0x800705b4.
|
||||
var snapshot = new EspPolicyProviderSnapshot(new[]
|
||||
{
|
||||
new PolicyProviderState("ConfigMgr", "device", EspPolicyProviderProbe.KindSetupApps, null, null),
|
||||
ConfigMgrDevicePrep(1),
|
||||
});
|
||||
using var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => snapshot);
|
||||
var (sut, sink, clock) = BuildDetector(phase: () => EspPhaseSnapshot.DevicePreparation);
|
||||
|
||||
sut.Tick();
|
||||
clock.Advance(DwellSpan);
|
||||
sut.Tick();
|
||||
|
||||
var evt = Assert.Single(StalledEvents(sink));
|
||||
var data = Assert.IsAssignableFrom<IReadOnlyDictionary<string, object>>(evt.TypedPayload);
|
||||
var stalled = Assert.IsAssignableFrom<IReadOnlyList<Dictionary<string, object>>>(data["stalledProviders"]);
|
||||
Assert.Contains(stalled, s => (string)s["kind"] == EspPolicyProviderProbe.KindDevicePreparation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Device_preparation_stall_message_names_the_provider_timeout()
|
||||
{
|
||||
var snapshot = new EspPolicyProviderSnapshot(new[] { SidecarDone(), ConfigMgrDevicePrep(1) });
|
||||
using var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => snapshot);
|
||||
var (sut, sink, clock) = BuildDetector(phase: () => EspPhaseSnapshot.DevicePreparation);
|
||||
|
||||
sut.Tick();
|
||||
clock.Advance(DwellSpan);
|
||||
sut.Tick();
|
||||
|
||||
var message = Assert.Single(StalledEvents(sink)).Payload![SignalPayloadKeys.Message];
|
||||
Assert.Contains("until their timeout", message);
|
||||
Assert.DoesNotContain("without any timeout", message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fingerprint_skips_device_preparation_providers_past_their_phase()
|
||||
{
|
||||
var logger = new AgentLogger(_tmp.Path, AgentLogLevel.Info);
|
||||
var phase = EspPhaseSnapshot.DevicePreparation;
|
||||
var devicePrepOnly = new EspPolicyProviderSnapshot(new[] { SidecarDone(), ConfigMgrDevicePrep(1) });
|
||||
var withContoso = new EspPolicyProviderSnapshot(new[]
|
||||
{
|
||||
SidecarDone(),
|
||||
ConfigMgrDevicePrep(1),
|
||||
new PolicyProviderState("Contoso", "device", EspPolicyProviderProbe.KindSetupApps, 0, null),
|
||||
});
|
||||
var current = devicePrepOnly;
|
||||
|
||||
// Run 1: the DevicePreparation provider stalls during device preparation; later, in
|
||||
// device setup, a Setup/Apps provider stalls as well.
|
||||
using (var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => current))
|
||||
{
|
||||
var (sut, sink, clock) = BuildDetector(new StartupEventGate(_tmp.Path, logger), () => phase);
|
||||
sut.Tick();
|
||||
clock.Advance(DwellSpan);
|
||||
sut.Tick();
|
||||
|
||||
phase = "device_setup";
|
||||
current = withContoso;
|
||||
sut.Tick();
|
||||
clock.Advance(DwellSpan);
|
||||
sut.Tick();
|
||||
Assert.Equal(2, StalledEvents(sink).Count);
|
||||
}
|
||||
|
||||
// Run 2 (restart in device setup): the same Contoso stall lands on the same
|
||||
// fingerprint only if the stale DevicePreparation provider stayed out of it.
|
||||
using (var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => withContoso))
|
||||
{
|
||||
var (sut, sink, clock) = BuildDetector(new StartupEventGate(_tmp.Path, logger), () => "device_setup");
|
||||
sut.Tick();
|
||||
clock.Advance(DwellSpan);
|
||||
sut.Tick();
|
||||
Assert.Empty(StalledEvents(sink));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Healthy_or_absent_tracking_never_emits()
|
||||
{
|
||||
|
||||
+16
-2
@@ -20,13 +20,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
{
|
||||
internal const string None = "none";
|
||||
internal const string Unavailable = "unavailable";
|
||||
internal const string DevicePreparation = "device_preparation";
|
||||
private const string FailedSuffix = "_failed";
|
||||
|
||||
internal enum CategoryState { Absent, Running, Done, Failed }
|
||||
|
||||
// ESP order: device preparation, device setup, account setup.
|
||||
private static readonly (string ValueName, string Phase)[] Categories =
|
||||
{
|
||||
("DevicePreparationCategory.Status", "device_preparation"),
|
||||
("DevicePreparationCategory.Status", DevicePreparation),
|
||||
("DeviceSetupCategory.Status", "device_setup"),
|
||||
("AccountSetupCategory.Status", "account_setup"),
|
||||
};
|
||||
@@ -60,7 +62,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
var states = Categories.Select((_, i) => Classify(i < statusJson.Count ? statusJson[i] : null)).ToList();
|
||||
|
||||
for (var i = 0; i < Categories.Length; i++)
|
||||
if (states[i] == CategoryState.Failed) return Categories[i].Phase + "_failed";
|
||||
if (states[i] == CategoryState.Failed) return Categories[i].Phase + FailedSuffix;
|
||||
for (var i = 0; i < Categories.Length; i++)
|
||||
if (states[i] == CategoryState.Running) return Categories[i].Phase;
|
||||
|
||||
@@ -69,6 +71,18 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
return lastDone == Categories.Length - 1 ? "esp_done" : Categories[lastDone].Phase + "_done";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether device preparation can still hold the ESP in a <see cref="Read"/> result: it
|
||||
/// runs or failed, or the phase is unknown (<see cref="None"/>, <see cref="Unavailable"/>).
|
||||
/// False once device preparation is done, whichever later phase the ESP is in.
|
||||
/// </summary>
|
||||
internal static bool DevicePreparationOpen(string phase) =>
|
||||
string.IsNullOrEmpty(phase)
|
||||
|| phase == None
|
||||
|| phase == Unavailable
|
||||
|| phase == DevicePreparation
|
||||
|| phase == DevicePreparation + FailedSuffix;
|
||||
|
||||
/// <summary>
|
||||
/// One category value: done (<c>categorySucceeded=true</c>, or every subcategory succeeded
|
||||
/// or notRequired), failed (<c>false</c>, or a failed subcategory), running (anything
|
||||
|
||||
+34
-9
@@ -39,6 +39,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
/// dispatch-guard exempt), never into decision state.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Phase gate: <c>DevicePreparation</c> providers count only while
|
||||
/// <see cref="EspPhaseSnapshot.DevicePreparationOpen"/> holds. Once device preparation is done
|
||||
/// their values hold nothing: a ConfigMgr client installed as a Win32 app registers its
|
||||
/// provider after its own install with <c>InstallationState=1</c>, and nothing completes it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Duplicate suppression: in-process one-shot per provider key, plus a
|
||||
/// <see cref="StartupEventGate"/> fingerprint over the sorted set of currently-stalled keys so
|
||||
/// an agent restart into the same stall does not re-report it — while a NEW provider joining
|
||||
@@ -75,6 +81,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
private readonly IClock _clock;
|
||||
private readonly StartupEventGate? _startupGate;
|
||||
private readonly TimeSpan _dwell;
|
||||
private readonly Func<string> _espPhaseReader;
|
||||
|
||||
private readonly Dictionary<string, DateTime> _firstSeenIncompleteUtc =
|
||||
new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -94,7 +101,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
AgentLogger logger,
|
||||
IClock clock,
|
||||
StartupEventGate? startupGate = null,
|
||||
TimeSpan? dwell = null)
|
||||
TimeSpan? dwell = null,
|
||||
Func<string>? espPhaseReader = null)
|
||||
{
|
||||
_sessionId = sessionId;
|
||||
_tenantId = tenantId;
|
||||
@@ -103,6 +111,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
_startupGate = startupGate;
|
||||
_dwell = dwell ?? TimeSpan.FromMinutes(DwellMinutes);
|
||||
_espPhaseReader = espPhaseReader ?? EspPhaseSnapshot.Read;
|
||||
}
|
||||
|
||||
/// <summary>Host timer callback and test seam — one probe + evaluation round.</summary>
|
||||
@@ -111,7 +120,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
var snapshot = EspPolicyProviderProbe.Read(_logger);
|
||||
var eval = EvaluateStallTransition(
|
||||
snapshot, _firstSeenIncompleteUtc, _firedKeys,
|
||||
_sidecarMissingSinceUtc, _sidecarMissingFired, _clock.UtcNow, _dwell);
|
||||
_sidecarMissingSinceUtc, _sidecarMissingFired, _clock.UtcNow, _dwell,
|
||||
EspPhaseSnapshot.DevicePreparationOpen(_espPhaseReader()));
|
||||
|
||||
_firstSeenIncompleteUtc.Clear();
|
||||
foreach (var kv in eval.UpdatedFirstSeenUtc)
|
||||
@@ -133,6 +143,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
/// <param name="sidecarMissingAlreadyFired">Arm-2 one-shot latch (never re-report).</param>
|
||||
/// <param name="nowUtc">Evaluation timestamp.</param>
|
||||
/// <param name="dwell">Continuous-condition threshold (shared by both arms).</param>
|
||||
/// <param name="devicePreparationOpen">Whether device preparation can still hold the ESP (<see cref="EspPhaseSnapshot.DevicePreparationOpen"/>); false drops <c>DevicePreparation</c> providers from arm 1.</param>
|
||||
internal static StallEvaluation EvaluateStallTransition(
|
||||
EspPolicyProviderSnapshot snapshot,
|
||||
IReadOnlyDictionary<string, DateTime> firstSeenIncompleteUtc,
|
||||
@@ -140,9 +151,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
DateTime? sidecarMissingSinceUtc,
|
||||
bool sidecarMissingAlreadyFired,
|
||||
DateTime nowUtc,
|
||||
TimeSpan dwell)
|
||||
TimeSpan dwell,
|
||||
bool devicePreparationOpen = true)
|
||||
{
|
||||
var result = new StallEvaluation();
|
||||
var result = new StallEvaluation { DevicePreparationOpen = devicePreparationOpen };
|
||||
|
||||
// Missing root key or no registered providers = normal early enrollment: clear all
|
||||
// dwell state (a condition must be continuously OBSERVED to accrue dwell).
|
||||
@@ -152,8 +164,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
// Arm 1 — a registered provider continuously incomplete.
|
||||
foreach (var provider in snapshot.Providers)
|
||||
{
|
||||
if (provider.IsComplete)
|
||||
continue; // completed → falls out of the dwell map (reset)
|
||||
if (provider.IsComplete || !result.Counts(provider))
|
||||
continue; // completed or past its phase → falls out of the dwell map (reset)
|
||||
|
||||
var firstSeen = firstSeenIncompleteUtc.TryGetValue(provider.Key, out var seen)
|
||||
? seen
|
||||
@@ -197,7 +209,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
// the same stall re-accrues dwell and lands on the same fingerprint → suppressed; a
|
||||
// new condition joining the stall changes it → re-emitted.
|
||||
var currentlyStalledKeys = snapshot.Providers
|
||||
.Where(p => !p.IsComplete && _firedKeys.Contains(p.Key))
|
||||
.Where(p => !p.IsComplete && eval.Counts(p) && _firedKeys.Contains(p.Key))
|
||||
.Select(p => p.Key)
|
||||
.ToList();
|
||||
if (_sidecarMissingFired && HasSetupAppsProvider(snapshot) && !snapshot.SidecarRegistered)
|
||||
@@ -259,9 +271,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
if (eval.SidecarMissingStalledForMinutes != null)
|
||||
data["sidecarMissingForMinutes"] = Math.Round(eval.SidecarMissingStalledForMinutes.Value, 1);
|
||||
|
||||
// The Setup/Apps wait has no timeout; device preparation gives up after the provider's
|
||||
// Timeout node (CSP default 15 min).
|
||||
var setupAppsWait = eval.SidecarMissingStalledForMinutes != null
|
||||
|| eval.NewlyStalled.Any(s => s.Provider.Kind == EspPolicyProviderProbe.KindSetupApps);
|
||||
var message =
|
||||
"ESP Setup/Apps tracking stalled: " + string.Join("; ", messageParts) +
|
||||
" — the ESP waits on its app-tracking providers without any timeout";
|
||||
"ESP policy provider stalled: " + string.Join("; ", messageParts) +
|
||||
(setupAppsWait
|
||||
? " — the ESP waits on its Setup/Apps providers without any timeout"
|
||||
: " — device preparation waits on its providers until their timeout");
|
||||
|
||||
_post.Emit(new EnrollmentEvent
|
||||
{
|
||||
@@ -298,6 +316,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
|
||||
/// <summary>Non-null when the sidecar-missing condition crossed the dwell threshold this round and was not yet reported (arm 2).</summary>
|
||||
public double? SidecarMissingStalledForMinutes { get; set; }
|
||||
|
||||
/// <summary>The phase gate of this round; the cross-restart fingerprint applies the same filter.</summary>
|
||||
public bool DevicePreparationOpen { get; set; }
|
||||
|
||||
/// <summary>Whether arm 1 considers <paramref name="provider"/> in this round.</summary>
|
||||
public bool Counts(PolicyProviderState provider)
|
||||
=> DevicePreparationOpen || provider.Kind != EspPolicyProviderProbe.KindDevicePreparation;
|
||||
}
|
||||
|
||||
internal readonly struct StalledProvider
|
||||
|
||||
@@ -365,6 +365,51 @@ public class DiagnosisHeadlineBuilderTests
|
||||
Assert.Equal(DiagnosisCauses.EspStepFailure, h.Cause);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_device_preparation_provider_is_no_culprit_for_a_later_apps_failure()
|
||||
{
|
||||
// A ConfigMgr client installed as a Win32 app registers its device-preparation provider
|
||||
// after device preparation ended; its stall explains no device-setup failure.
|
||||
var events = new Timeline()
|
||||
.Add("esp_policy_provider_stalled", new()
|
||||
{
|
||||
["reason"] = "provider_incomplete",
|
||||
["stalledProviders"] = new List<object>
|
||||
{
|
||||
new Dictionary<string, object> { ["name"] = "ConfigMgr", ["scope"] = "device", ["kind"] = "devicePreparation", ["stalledForMinutes"] = 15.0 },
|
||||
},
|
||||
})
|
||||
.Add("enrollment_failed", Failed("DeviceSetup", "Apps"));
|
||||
|
||||
var h = DiagnosisHeadlineBuilder.Build(Session(SessionStatus.Failed, VerdictPaths.AgentFailed), events.Events);
|
||||
|
||||
Assert.Null(h.Culprits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Only_device_preparation_providers_explain_a_device_preparation_failure()
|
||||
{
|
||||
// Field shape: the ESP never left device preparation (0x800705b4).
|
||||
var events = new Timeline()
|
||||
.Add("esp_policy_provider_stalled", new()
|
||||
{
|
||||
["reason"] = "sidecar_provider_missing",
|
||||
["stalledProviders"] = new List<object>
|
||||
{
|
||||
new Dictionary<string, object> { ["name"] = "ConfigMgr", ["scope"] = "device", ["kind"] = "devicePreparation", ["stalledForMinutes"] = 40.0 },
|
||||
new Dictionary<string, object> { ["name"] = "Contoso", ["scope"] = "device", ["kind"] = "setupApps", ["stalledForMinutes"] = 40.0 },
|
||||
},
|
||||
})
|
||||
.Add("enrollment_failed", Failed("DevicePreparation", "EspProviderInstallation", "0x800705b4"));
|
||||
|
||||
var h = DiagnosisHeadlineBuilder.Build(Session(SessionStatus.Failed, VerdictPaths.AgentFailed), events.Events);
|
||||
|
||||
var provider = Assert.Single(h.Culprits!);
|
||||
Assert.Equal(DiagnosisCulpritKinds.PolicyProvider, provider.Kind);
|
||||
Assert.Equal("ConfigMgr", provider.Name);
|
||||
Assert.Equal(40.0, provider.StalledForMinutes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_appx_log_candidate_joins_the_culprits()
|
||||
{
|
||||
|
||||
+23
-3
@@ -18,6 +18,10 @@ internal static class DiagnosisCulpritFinder
|
||||
|
||||
private static readonly IReadOnlyList<string> EvidenceRank = DiagnosisCulpritEvidence.All;
|
||||
|
||||
// The agent's provider kinds (EspPolicyProviderProbe.KindDevicePreparation / KindSetupApps).
|
||||
private const string DevicePreparationProviderKind = "devicePreparation";
|
||||
private const string SetupAppsProviderKind = "setupApps";
|
||||
|
||||
internal static List<DiagnosisCulprit> Find(
|
||||
IReadOnlyList<EnrollmentEvent> events, FailureEvidence failure, EspBlockingSets? blocking)
|
||||
{
|
||||
@@ -89,7 +93,7 @@ internal static class DiagnosisCulpritFinder
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var provider in StalledProviders(events))
|
||||
foreach (var provider in StalledProviders(events, failure.Step.Category))
|
||||
Add(provider);
|
||||
|
||||
return culprits
|
||||
@@ -125,14 +129,21 @@ internal static class DiagnosisCulpritFinder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stuck ESP policy providers from the latest <c>esp_policy_provider_stalled</c>.</summary>
|
||||
internal static IEnumerable<DiagnosisCulprit> StalledProviders(IReadOnlyList<EnrollmentEvent> events)
|
||||
/// <summary>
|
||||
/// Stuck ESP policy providers from the latest <c>esp_policy_provider_stalled</c>. Given the
|
||||
/// failed ESP category, only providers that can hold it count: device-preparation providers
|
||||
/// for a device-preparation failure, Setup/Apps providers for device or account setup. A
|
||||
/// ConfigMgr client installed as a Win32 app registers its device-preparation provider after
|
||||
/// device preparation ended, so that provider explains no later failure.
|
||||
/// </summary>
|
||||
internal static IEnumerable<DiagnosisCulprit> StalledProviders(IReadOnlyList<EnrollmentEvent> events, string? failedCategory = null)
|
||||
{
|
||||
var stalled = DiagnosisEspEvidence.LastOf(events, e => e.EventType == Constants.EventTypes.EspPolicyProviderStalled);
|
||||
foreach (var item in EventDataReader.Maps(stalled?.Data, "stalledProviders"))
|
||||
{
|
||||
var name = DiagnosisEspEvidence.NonEmpty(EventDataReader.String(item, "name"));
|
||||
if (name == null) continue;
|
||||
if (!CanHoldCategory(EventDataReader.String(item, "kind"), failedCategory)) continue;
|
||||
yield return new DiagnosisCulprit
|
||||
{
|
||||
Kind = DiagnosisCulpritKinds.PolicyProvider,
|
||||
@@ -146,6 +157,15 @@ internal static class DiagnosisCulpritFinder
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanHoldCategory(string? kind, string? failedCategory)
|
||||
{
|
||||
if (string.IsNullOrEmpty(failedCategory) || string.IsNullOrEmpty(kind)) return true;
|
||||
var devicePreparationFailed = string.Equals(failedCategory, "DevicePreparation", StringComparison.OrdinalIgnoreCase);
|
||||
if (string.Equals(kind, DevicePreparationProviderKind, StringComparison.OrdinalIgnoreCase)) return devicePreparationFailed;
|
||||
if (string.Equals(kind, SetupAppsProviderKind, StringComparison.OrdinalIgnoreCase)) return !devicePreparationFailed;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DiagnosisCulprit AppCulprit(AppTrack app, string evidence, EspBlockingSets? blocking, string? eventId) => new()
|
||||
{
|
||||
Kind = DiagnosisCulpritKinds.App,
|
||||
|
||||
@@ -741,7 +741,7 @@ namespace AutopilotMonitor.Shared
|
||||
public const string TelemetryUploadBlocked = "telemetry_upload_blocked"; // Warning (one-shot each) when the drain is blocked by a retained unknown permanent 4xx (400 contract bug / 404 route mismatch / tenant-validation), or by an interception on the network path (data.reason client_certificate_required | foreign_response, plus operation, statusCode, pageTitle, serverCertIssuer, firstBlockedAtUtc). Data is NOT discarded. LOCAL diagnostic (agent log + diag ZIP): this marker is queued behind the blocking batch and only reaches the backend after the block clears — no out-of-band bypass by design.
|
||||
public const string SessionParkedWithoutDeadline = "session_parked_without_deadline"; // One-shot Warning tripwire (liveness plan PR1): a non-terminal session entered the post-AccountSetup dead-end zone (ESP final exit or advisory failure) with NO resolution-capable deadline armed. After the 2026-06-12 dead-end fixes this must never fire in production — every occurrence is a bug report for an unknown parking variant.
|
||||
public const string DiskSpaceLow = "disk_space_low"; // One-shot Warning (state-change-only, 1 GB hysteresis — NOT periodic) when the system drive drops below 2 GB free. Review MON-B10: disk-full was previously only visible inside Debug performance_snapshot payloads (filtered out of timelines), so a disk-starved enrollment carried no actionable threshold signal.
|
||||
public const string EspPolicyProviderStalled = "esp_policy_provider_stalled"; // One-shot-per-provider Warning: a registered EnrollmentStatusTracking CSP policy provider stayed incomplete >= 15 min (Setup/Apps TrackingPoliciesCreated != 1, or DevicePreparation InstallationState not in {2,3}). The ESP waits on Setup/Apps providers WITHOUT any OS timeout — e.g. pre-provisioning + co-management leaves only "ConfigMgr" registered (no "Sidecar" = IME) and the user ESP sits at "Apps (Identifying)" for days. Observational only; payload carries the full provider list. See EspPolicyProviderProbe.
|
||||
public const string EspPolicyProviderStalled = "esp_policy_provider_stalled"; // One-shot-per-provider Warning: a registered EnrollmentStatusTracking CSP policy provider stayed incomplete >= 15 min (Setup/Apps TrackingPoliciesCreated != 1, or DevicePreparation InstallationState not in {2,3} while device preparation is still open). The ESP waits on Setup/Apps providers WITHOUT any OS timeout — e.g. pre-provisioning + co-management leaves only "ConfigMgr" registered (no "Sidecar" = IME) and the user ESP sits at "Apps (Identifying)" for days. Observational only; payload carries the full provider list. See EspPolicyProviderProbe.
|
||||
public const string KeepAwakeEngaged = "keep_awake_engaged"; // Info (state-change, NOT periodic) emitted by the opt-in UserEspKeepAwake host when it holds the device awake (system + display) on entering the User-ESP/AccountSetup phase, so idle standby cannot stall app installs / account provisioning. Reboots are unaffected. Per-tenant toggle (AnalyzerConfiguration.KeepAwakeDuringUserEsp, default off).
|
||||
public const string KeepAwakeReleased = "keep_awake_released"; // Info (Warning when reason=safety_cap) emitted when the keep-awake hold is released — reason = user_esp_exited (Shell-Core 62407 after AccountSetup progress, i.e. the user-ESP page closed; agents before 2026-09-04 wrote account_setup_complete for the synthesized completion, which fired minutes early) | host_stop | safety_cap. Brackets KeepAwakeEngaged; the OS also auto-clears the hold on process exit/reboot.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user