mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(agent): stop Office detector false-failed; defer completion to TasksState capture
Field session 8353e03b declared a successful Office install as failed. Root cause (diag zip + do-status.jsonl + registry): - OfficeC2RClient.exe is a transient launcher (~2s in the field) that kicks off the operation; the OfficeClickToRun.exe service installs for minutes after it exits. Treating worker-exit as completion → false office_install_failed. - The registry has NO StreamingFinished value (so the completion branch could never fire and the "abandoned" branch always did), and Scenario\INSTALL persists post-install (so presence is not an "active" signal). - Office C2R content comes from f.c2r.ts.cdn.office.net AND officecdn.microsoft.com; the old DO matcher only checked "officecdn" and would never attribute Office jobs. Conservative fix (completion/failure detection deferred until a live-install capture of Scenario\INSTALL\TasksState transitions — the real signal: per-task TASKSTATE_COMPLETED/_INPROGRESS): - OnWorkerStopped no longer finalizes — emits no terminal. started + progress only. - Removed the StreamingFinished-based completed/abandoned logic; only an explicit numeric error code finalizes as failed. - phase label is "Installing" (cannot distinguish Streaming/Finalizing without a real signal). - DO Office matcher now keys on the stable CDN hosts cdn.office.net + officecdn.microsoft.com (dropped the version-fragile _16_0_ FileId marker). V2 suite 1257/1257 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-47
@@ -122,7 +122,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
|
||||
Assert.Equal("Info", Severity(started));
|
||||
|
||||
var data = Data(started);
|
||||
Assert.Equal("Streaming", data["phase"]);
|
||||
Assert.Equal("Installing", data["phase"]);
|
||||
Assert.Equal("INSTALL", data["scenario"]);
|
||||
Assert.Contains("O365ProPlusRetail", (List<string>)data["products"]);
|
||||
Assert.Equal("Current", data["channel"]);
|
||||
@@ -220,71 +220,45 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
|
||||
Assert.Equal(2, events.Count); // started + one progress only
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- completed
|
||||
// ---------------------------------------------------------------- terminal (conservative v1)
|
||||
|
||||
[Fact]
|
||||
public void Worker_stopped_with_streaming_finished_emits_completed_with_version_and_duration()
|
||||
public void Worker_stopped_emits_no_terminal_event()
|
||||
{
|
||||
// Conservative v1: the transient worker's exit is NOT install completion — no completed/
|
||||
// failed is emitted (field session 8353e03b false-failed). started/progress only.
|
||||
using var rig = new Rig(ActiveStreaming());
|
||||
|
||||
rig.Sut.OnWorkerStarted(); // started
|
||||
rig.Clock.Advance(TimeSpan.FromSeconds(412));
|
||||
rig.Current = CompletedIdle("16.0.17628.20144");
|
||||
rig.Sut.OnWorkerStopped(); // completed
|
||||
|
||||
var events = rig.OfficeEvents();
|
||||
Assert.Equal(2, events.Count);
|
||||
var completed = events[1];
|
||||
Assert.Equal(Constants.EventTypes.OfficeInstallCompleted, EventType(completed));
|
||||
Assert.Equal("Info", Severity(completed));
|
||||
|
||||
var data = Data(completed);
|
||||
Assert.Equal("Completed", data["phase"]);
|
||||
Assert.Equal("16.0.17628.20144", data["versionReached"]);
|
||||
Assert.Equal(412, data["durationSeconds"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Terminal_state_latches_no_further_events()
|
||||
{
|
||||
using var rig = new Rig(ActiveStreaming());
|
||||
|
||||
rig.Sut.OnWorkerStarted();
|
||||
rig.Sut.OnWorkerStarted(); // started
|
||||
rig.Current = CompletedIdle();
|
||||
rig.Sut.OnWorkerStopped(); // completed
|
||||
rig.Current = ActiveStreaming();
|
||||
rig.Sut.OnWorkerStarted(); // must NOT re-trigger (one lifecycle)
|
||||
rig.Sut.OnRegistryChanged();
|
||||
rig.Sut.OnWorkerStopped(); // must NOT emit a terminal
|
||||
|
||||
var events = rig.OfficeEvents();
|
||||
Assert.Equal(2, events.Count);
|
||||
Assert.Equal(Constants.EventTypes.OfficeInstallCompleted, EventType(events[1]));
|
||||
Assert.Single(events);
|
||||
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(events[0]));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- failed
|
||||
|
||||
[Fact]
|
||||
public void Worker_stopped_without_streaming_finished_emits_failed_warning()
|
||||
public void After_worker_stopped_further_events_are_ignored()
|
||||
{
|
||||
using var rig = new Rig(ActiveStreaming());
|
||||
|
||||
rig.Sut.OnWorkerStarted();
|
||||
rig.Sut.OnWorkerStarted(); // started
|
||||
rig.Sut.OnWorkerStopped(); // stop tracking (no terminal)
|
||||
|
||||
rig.Current = new OfficeC2RSnapshot
|
||||
{
|
||||
ConfigurationKeyPresent = true,
|
||||
StreamingFinished = false,
|
||||
ActiveScenarioPresent = false,
|
||||
OfficeC2RClientRunning = false,
|
||||
};
|
||||
rig.Sut.OnWorkerStopped(); // failed (abandoned)
|
||||
// A brand-new worker / registry change must not re-trigger (one lifecycle).
|
||||
rig.Current = ActiveStreaming();
|
||||
rig.Sut.OnWorkerStarted();
|
||||
rig.Sut.OnRegistryChanged();
|
||||
rig.Sut.OnOfficeDoSample(new OfficeDoSample { JobCount = 1, FileSize = 100, TotalBytesDownloaded = 50 });
|
||||
|
||||
var events = rig.OfficeEvents();
|
||||
Assert.Equal(2, events.Count);
|
||||
Assert.Equal(Constants.EventTypes.OfficeInstallFailed, EventType(events[1]));
|
||||
Assert.Equal("Warning", Severity(events[1]));
|
||||
Assert.Single(events);
|
||||
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(events[0]));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- failed (error code only)
|
||||
|
||||
[Fact]
|
||||
public void Observed_error_code_during_progress_emits_failed_error_with_code()
|
||||
{
|
||||
|
||||
+29
-33
@@ -30,9 +30,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
|
||||
/// <item><see cref="OnOfficeDoSample"/> — aggregated Delivery-Optimization stats for Office's CDN
|
||||
/// download (from the extended DeliveryOptimizationCollector). DO has no push API, so it is
|
||||
/// sampled while the worker is alive; a byte advance folds a real download-% into progress.</item>
|
||||
/// <item><see cref="OnWorkerStopped"/> — last worker exited (<see cref="Process.Exited"/>) →
|
||||
/// emit <see cref="Constants.EventTypes.OfficeInstallCompleted"/> /
|
||||
/// <see cref="Constants.EventTypes.OfficeInstallFailed"/> with version + duration + DO summary.</item>
|
||||
/// <item><see cref="OnWorkerStopped"/> — last worker exited. NOT treated as completion (the
|
||||
/// worker is a transient launcher; the C2R service installs on for minutes). Conservative v1:
|
||||
/// stop tracking, emit no terminal. Completion/failure (via Scenario\INSTALL\TasksState) is
|
||||
/// deferred to the live-install capture iteration. Only an explicit numeric error code finalizes
|
||||
/// as <see cref="Constants.EventTypes.OfficeInstallFailed"/>.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -134,15 +136,25 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Last Office C2R worker exited — resolve terminal completed / failed.</summary>
|
||||
/// <summary>
|
||||
/// Last Office C2R worker exited. The worker (OfficeC2RClient.exe) is a transient launcher —
|
||||
/// in the field (session 8353e03b) it lived only ~2s and kicked off the operation, while the
|
||||
/// OfficeClickToRun.exe service ran the actual install for minutes afterwards. Its exit is
|
||||
/// therefore NOT install completion. Emitting failed on worker-exit was a false-positive, so
|
||||
/// we do NOT finalize here — we simply stop tracking and emit nothing.
|
||||
/// <para>
|
||||
/// Completion/failure detection is deferred (conservative v1): the registry has NO
|
||||
/// <c>StreamingFinished</c> value and <c>Scenario\INSTALL</c> persists post-install, so neither
|
||||
/// is a usable signal. The real completion signal is <c>Scenario\INSTALL\TasksState</c>
|
||||
/// (per-task <c>TASKSTATE_COMPLETED</c> / <c>_INPROGRESS</c> / failure); the in-progress/failed
|
||||
/// wording is being captured from a live install before it is wired up — see office-followup.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void OnWorkerStopped()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_state != DetectorState.Active) return;
|
||||
var snap = ReadSnapshotSafe();
|
||||
if (snap == null) snap = new OfficeC2RSnapshot();
|
||||
Finalize(snap);
|
||||
if (_state == DetectorState.Active) _state = DetectorState.Terminal; // stop; no terminal event
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,9 +164,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
|
||||
|
||||
private void EvaluateActive(OfficeC2RSnapshot snap)
|
||||
{
|
||||
// Conservative v1: the only terminal we trust is an explicit numeric error code. Completion
|
||||
// is NOT claimed (TasksState-based detection lands with the live-install capture).
|
||||
if (!string.IsNullOrEmpty(snap.ErrorCode))
|
||||
{
|
||||
Finalize(snap);
|
||||
FinalizeFailed(snap);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,33 +180,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
|
||||
}
|
||||
}
|
||||
|
||||
private void Finalize(OfficeC2RSnapshot snap)
|
||||
private void FinalizeFailed(OfficeC2RSnapshot snap)
|
||||
{
|
||||
if (_state == DetectorState.Terminal) return;
|
||||
_state = DetectorState.Terminal;
|
||||
|
||||
string eventType;
|
||||
EventSeverity severity;
|
||||
string phase;
|
||||
if (!string.IsNullOrEmpty(snap.ErrorCode))
|
||||
{
|
||||
eventType = Constants.EventTypes.OfficeInstallFailed; severity = EventSeverity.Error; phase = "Failed";
|
||||
}
|
||||
else if (snap.StreamingFinished)
|
||||
{
|
||||
eventType = Constants.EventTypes.OfficeInstallCompleted; severity = EventSeverity.Info; phase = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Worker gone without StreamingFinished → abandoned install.
|
||||
eventType = Constants.EventTypes.OfficeInstallFailed; severity = EventSeverity.Warning; phase = "Failed";
|
||||
}
|
||||
|
||||
EmitLifecycle(eventType, snap, severity, phase, isTerminal: true);
|
||||
EmitLifecycle(Constants.EventTypes.OfficeInstallFailed, snap, EventSeverity.Error, "Failed", isTerminal: true);
|
||||
}
|
||||
|
||||
private static string PhaseOf(OfficeC2RSnapshot snap)
|
||||
=> snap.StreamingFinished ? "Finalizing" : "Streaming";
|
||||
// Conservative v1: StreamingFinished does not exist in the C2R registry, so we cannot label
|
||||
// Streaming vs Finalizing — report a single "Installing" phase until TasksState-based phase
|
||||
// detection is added.
|
||||
private static string PhaseOf(OfficeC2RSnapshot snap) => "Installing";
|
||||
|
||||
/// <summary>
|
||||
/// Stable signature of everything we treat as "progress". A change in any of these (registry
|
||||
@@ -203,8 +201,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
"phase=" + PhaseOf(snap),
|
||||
"streaming=" + snap.StreamingFinished,
|
||||
"version=" + (snap.VersionToReport ?? string.Empty),
|
||||
"scenario=" + (snap.ActiveScenarioName ?? string.Empty),
|
||||
};
|
||||
|
||||
+11
-5
@@ -469,16 +469,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic
|
||||
$"{progressCount} progress updates, {matchCount} new matches ({_enrichedAppIds.Count} total enriched)");
|
||||
}
|
||||
|
||||
// Office C2R content CDN hosts — version-independent (a FileId build marker like "_16_0_" would
|
||||
// break on older/newer Office versions). Field-validated against session 8353e03b: C2R uses BOTH
|
||||
// the primary CDN officecdn.microsoft.com (registry OriginalCDNDomain) and the content CDN
|
||||
// f.c2r.ts.cdn.office.net (registry FailoverDomain / PreferredCDNPrefix).
|
||||
private static readonly string[] OfficeCdnHosts = { "cdn.office.net", "officecdn.microsoft.com" };
|
||||
|
||||
/// <summary>
|
||||
/// True when a DO job's SourceURL points at the Office CDN (Office C2R content), i.e. a job
|
||||
/// that is not an IME Win32 app. TODO(office-followup): validate the exact SourceURL/FileId
|
||||
/// shape against a real enrollment's do-status.jsonl and tighten if needed.
|
||||
/// True when a DO job's SourceURL points at an Office C2R content CDN (not an IME Win32 app).
|
||||
/// Matched purely by the stable CDN host so it survives Office version changes.
|
||||
/// </summary>
|
||||
private static bool IsOfficeCdnJob(string sourceUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceUrl)) return false;
|
||||
return sourceUrl.IndexOf("officecdn", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| sourceUrl.IndexOf("officeclient.microsoft.com", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
foreach (var host in OfficeCdnHosts)
|
||||
if (sourceUrl.IndexOf(host, StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user