mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
fix(agent/v2): defer RealmJoin package-watcher arming until DeploymentPhase >= 100
Previously the HKLM\SOFTWARE\RealmJoin (+ HKU\<sid>\...) subtree watchers attached the
moment the Services\realmjoin gate fired. The window between "RJ installed" and "RJ
actually deploying" is exactly when IME may still be writing to the same SOFTWARE\RealmJoin
subtree as part of unrelated package installs — those writes were being misattributed
to RJ's package lifecycle (realmjoin_package_started / _completed events for packages
RJ never deployed).
Defer the package watchers until RJ has moved out of Blank, i.e. DeploymentPhase >=
PhaseRunningThresholdMin (100 = RunningFirstDeployment, RunningFirstDeploymentAuto,
RunningDeployment, ManualDeployment, NestedFromShim, or terminal CompletedFirstDeployment).
The realmjoin_detected event still fires on service-key appearance (no change to the
detection gate); only the package-tracking surface stays disarmed for the pre-running
window.
- New const RealmJoinInfo.PhaseRunningThresholdMin = 100.
- Watcher state flag _packageWatchersArmed (set-once); old _realmJoinServiceDetected
removed (no remaining readers).
- AttachServiceRealmjoinSubtreeWatcher no longer arms the package watchers — it only
attaches the Services\realmjoin DeploymentPhase watcher and fires Detected.
- NotifyRealmJoinPresence arms HKLM + (if SID known) HKU package watchers on the first
phase >= 100 observation; set-once so a later regressed reading does not disarm.
- ArmHku now defers on _packageWatchersArmed instead of the dropped service-detected flag.
Tests: +3 (disarmed-during-blank, set-once-after-running, immediate-arm-on-boot-at-110).
14/14 RealmJoin adapter suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+58
@@ -224,6 +224,64 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters
|
||||
Assert.Equal("0", info.Payload["lastExitCode"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Package_watchers_stay_disarmed_until_phase_reaches_RunningThreshold()
|
||||
{
|
||||
// Pre-RJ window: IME may still be writing to HKLM\SOFTWARE\RealmJoin while the
|
||||
// RJ service key is already present but DeploymentPhase is still Blank (0).
|
||||
// Package watchers must NOT arm yet — otherwise IME's in-flight package writes
|
||||
// would be misattributed to RJ's lifecycle.
|
||||
using var f = new Fixture();
|
||||
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
|
||||
|
||||
// Service-appearance fires Detected without a phase value — must NOT arm.
|
||||
f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: null);
|
||||
Assert.False(f.Watcher.PackageWatchersArmedForTest);
|
||||
|
||||
// Phase = 0 (Blank) — still no arm; RJ is installed but not deploying.
|
||||
f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 0);
|
||||
Assert.False(f.Watcher.PackageWatchersArmedForTest);
|
||||
|
||||
// Phase transitions to 100 (RunningFirstDeployment) — package watchers MUST arm now.
|
||||
f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 100);
|
||||
Assert.True(f.Watcher.PackageWatchersArmedForTest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Package_watchers_stay_armed_once_phase_subsequently_drops_back_below_threshold()
|
||||
{
|
||||
// Set-once semantics — even if a later phase reading were below threshold
|
||||
// (defensive against partial / racy registry writes), the watcher does not
|
||||
// disarm and re-introduce a window where post-running events could be lost.
|
||||
using var f = new Fixture();
|
||||
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
|
||||
|
||||
f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 100);
|
||||
Assert.True(f.Watcher.PackageWatchersArmedForTest);
|
||||
|
||||
// Hypothetical regression: phase reads as 0 again — must not flip the flag.
|
||||
f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 0);
|
||||
Assert.True(f.Watcher.PackageWatchersArmedForTest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Package_watchers_arm_immediately_when_first_phase_observed_is_completed()
|
||||
{
|
||||
// Agent boots into a session where RJ is already at CompletedFirstDeployment
|
||||
// (110) — e.g. pre-installed on an image, or recovered after a long absence.
|
||||
// The first phase observation is already past threshold, so package watchers
|
||||
// arm in that same notify pass; the historic package rows enumerated by
|
||||
// CheckMachinePackages then surface as started+completed pairs.
|
||||
using var f = new Fixture();
|
||||
using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
|
||||
|
||||
f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 110);
|
||||
|
||||
Assert.True(f.Watcher.PackageWatchersArmedForTest);
|
||||
// Resolved fires on the same pass.
|
||||
Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.RealmJoinResolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Watcher_fires_PackageStarted_even_when_DisplayName_is_missing()
|
||||
{
|
||||
|
||||
+8
@@ -66,6 +66,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
// Resolved-phase value per the RJ enum (RealmJoin.Core.SoftwarePackaging.DeploymentPhase).
|
||||
public const int PhaseCompletedFirstDeployment = 110;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum DWORD value indicating RJ has moved out of <c>Blank (0)</c> into an active
|
||||
/// or terminal deployment phase. Below this the SOFTWARE\RealmJoin subtree may still
|
||||
/// be written by adjacent IME packages — package watchers stay disarmed until RJ owns
|
||||
/// the keys.
|
||||
/// </summary>
|
||||
public const int PhaseRunningThresholdMin = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Map a raw <c>DeploymentPhase</c> DWORD to its RJ-internal enum name. Mirrors
|
||||
/// <c>RealmJoin.Core.SoftwarePackaging.DeploymentPhase</c> as documented at feature
|
||||
|
||||
+45
-22
@@ -118,10 +118,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
private readonly HashSet<string> _hkuPackagesCompleted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private string? _hkuSid;
|
||||
// Set once the Services\realmjoin gate fires. Until then, HKLM\SOFTWARE\RealmJoin and
|
||||
// HKU\<sid>\SOFTWARE\RealmJoin remain unarmed — RJ presence is the prerequisite for
|
||||
// looking at those keys at all.
|
||||
private bool _realmJoinServiceDetected;
|
||||
// Set once the first non-Blank DeploymentPhase (>= 100) is observed. Package watchers
|
||||
// (HKLM\SOFTWARE\RealmJoin + HKU\<sid>\SOFTWARE\RealmJoin) only arm at that point —
|
||||
// earlier observation would mix IME's in-flight package writes (which share the same
|
||||
// subtree) into RJ's lifecycle.
|
||||
private bool _packageWatchersArmed;
|
||||
private bool _disposed;
|
||||
|
||||
public event EventHandler<RealmJoinDetectedEventArgs>? RealmJoinDetected;
|
||||
@@ -151,10 +152,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
/// <see cref="DesktopArrivalDetector"/> resolves a real user owner and
|
||||
/// <see cref="UserSidResolver"/> produces the SID. Idempotent.
|
||||
/// <para>
|
||||
/// The actual HKU watcher only arms once the <c>Services\realmjoin</c> gate fires —
|
||||
/// either it has already fired (immediate arm) or it fires later
|
||||
/// (deferred arm from <see cref="AttachServiceRealmjoinSubtreeWatcher"/>). On devices
|
||||
/// without RJ the SID is recorded but no HKU watcher is ever created.
|
||||
/// The actual HKU watcher only arms once the package-watcher arming threshold has
|
||||
/// fired (RJ observed at <see cref="RealmJoinInfo.PhaseRunningThresholdMin"/> or
|
||||
/// higher). Either it has already fired (immediate arm) or fires later
|
||||
/// (deferred arm from <see cref="NotifyRealmJoinPresence"/>). On devices without RJ
|
||||
/// or where RJ never leaves Blank, the SID is recorded but no HKU watcher is created.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void ArmHku(string sid)
|
||||
@@ -167,7 +169,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
{
|
||||
if (_hkuSid != null) return;
|
||||
_hkuSid = sid;
|
||||
armNow = _realmJoinServiceDetected;
|
||||
armNow = _packageWatchersArmed;
|
||||
}
|
||||
_logger.Info($"RealmJoinWatcher: recorded SID {sid} for HKU watching (armNow={armNow})");
|
||||
|
||||
@@ -337,7 +339,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
|
||||
private void AttachServiceRealmjoinSubtreeWatcher()
|
||||
{
|
||||
bool armUserToo;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_serviceRealmjoinWatcher != null) return;
|
||||
@@ -361,23 +362,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
{
|
||||
_logger.Warning($"RealmJoinWatcher: failed to attach Services\\realmjoin watcher: {ex.Message}");
|
||||
}
|
||||
|
||||
_realmJoinServiceDetected = true;
|
||||
armUserToo = _hkuSid != null;
|
||||
}
|
||||
|
||||
// The appearance of Services\realmjoin is itself the RJ-detected signal —
|
||||
// surface it even if Parameters\DeploymentPhase isn't populated yet.
|
||||
// surface it even if Parameters\DeploymentPhase isn't populated yet. The package
|
||||
// watchers (HKLM + HKU SOFTWARE\RealmJoin) intentionally stay disarmed here:
|
||||
// NotifyRealmJoinPresence arms them once DeploymentPhase >= PhaseRunningThresholdMin
|
||||
// is observed. Before that the same subtree may still receive in-flight IME
|
||||
// package writes that are not part of RJ's lifecycle.
|
||||
NotifyRealmJoinPresence(phase: null);
|
||||
CheckParameters();
|
||||
|
||||
// RJ is now confirmed installed — arm the package watchers.
|
||||
EnsureMachineRealmJoinStage();
|
||||
if (armUserToo)
|
||||
{
|
||||
CheckUserPackages();
|
||||
EnsureUserRealmJoinStage();
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachMachineRealmJoinSubtreeWatcher()
|
||||
@@ -469,6 +463,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
bool firePhaseChange = false;
|
||||
bool fireResolved = false;
|
||||
int? prevPhase = null;
|
||||
bool armPackageWatchers = false;
|
||||
bool armUserPackageWatcherToo = false;
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
@@ -488,6 +484,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
fireResolved = true;
|
||||
_resolvedFired = true;
|
||||
}
|
||||
// Arm the package watchers once RJ leaves Blank (phase >= 100). Before that,
|
||||
// IME may still be writing to HKLM\SOFTWARE\RealmJoin during the pre-RJ
|
||||
// install window and we'd misattribute those rows to RJ's lifecycle.
|
||||
if (!_packageWatchersArmed && phase.Value >= RealmJoinInfo.PhaseRunningThresholdMin)
|
||||
{
|
||||
_packageWatchersArmed = true;
|
||||
armPackageWatchers = true;
|
||||
armUserPackageWatcherToo = _hkuSid != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +520,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
try { RealmJoinResolved?.Invoke(this, new RealmJoinResolvedEventArgs(phase.Value)); }
|
||||
catch (Exception ex) { _logger.Error("RealmJoinWatcher: RealmJoinResolved handler threw", ex); }
|
||||
}
|
||||
|
||||
if (armPackageWatchers)
|
||||
{
|
||||
_logger.Info($"RealmJoinWatcher: phase={phase} crossed RunningThreshold — arming package watchers (HKLM + HKU={armUserPackageWatcherToo})");
|
||||
EnsureMachineRealmJoinStage();
|
||||
if (armUserPackageWatcherToo)
|
||||
{
|
||||
CheckUserPackages();
|
||||
EnsureUserRealmJoinStage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckMachinePackages()
|
||||
@@ -649,5 +665,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
|
||||
internal void TriggerUserPackageObservationFromTest(RealmJoinPackageSnapshot snap) =>
|
||||
MaybeFirePackageEvents(RealmJoinPackageScope.User, snap, _hkuPackagesStarted, _hkuPackagesCompleted);
|
||||
|
||||
internal void TriggerNotifyRealmJoinPresenceFromTest(int? phase) => NotifyRealmJoinPresence(phase);
|
||||
|
||||
internal bool PackageWatchersArmedForTest
|
||||
{
|
||||
get { lock (_stateLock) { return _packageWatchersArmed; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user