agent bug fixes, trace events, and important IME events emitted now (user_session_completed)

This commit is contained in:
Oliver Kieselbach
2026-03-06 23:02:27 +01:00
parent c85bccae94
commit 44b4c2cb63
9 changed files with 126 additions and 23 deletions
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management;
using System.Threading;
@@ -17,6 +18,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
private readonly AgentLogger _logger;
private Timer _pollingTimer;
private bool _desktopArrived;
private bool _excludedUserTraced; // Emit trace event only once for excluded-user skips
private const int PollingIntervalSeconds = 30;
/// <summary>
@@ -38,6 +40,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
/// </summary>
public event EventHandler DesktopArrived;
/// <summary>
/// Optional callback for trace events (decision, reason, context).
/// Wired by MonitoringService to emit agent_trace events to the backend.
/// </summary>
public Action<string, string, Dictionary<string, object>> OnTraceEvent { get; set; }
public DesktopArrivalDetector(AgentLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -83,6 +91,20 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
if (IsExcludedUser(owner))
{
_logger.Debug($"DesktopArrivalDetector: explorer.exe PID {proc.Id} owned by excluded user '{owner}' — skipping");
// Trace this decision once so it's visible in the backend
if (!_excludedUserTraced)
{
_excludedUserTraced = true;
try
{
OnTraceEvent?.Invoke(
"desktop_excluded_user",
$"explorer.exe found but owned by excluded user '{owner}' — not a real user desktop",
new Dictionary<string, object> { { "pid", proc.Id }, { "session", proc.SessionId }, { "owner", owner } });
}
catch { }
}
continue;
}
@@ -90,6 +112,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
_desktopArrived = true;
_logger.Info($"DesktopArrivalDetector: real user desktop detected (explorer.exe PID {proc.Id}, session {proc.SessionId}, user '{owner}')");
try
{
OnTraceEvent?.Invoke(
"desktop_real_user_detected",
$"Real user desktop detected (explorer.exe PID {proc.Id}, user '{owner}')",
new Dictionary<string, object> { { "pid", proc.Id }, { "session", proc.SessionId }, { "owner", owner } });
}
catch { }
// Stop polling
_pollingTimer?.Dispose();
_pollingTimer = null;
@@ -106,6 +106,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// Desktop arrival detector — detects real user desktop for no-ESP completion
_desktopArrivalDetector = new DesktopArrivalDetector(_logger);
_desktopArrivalDetector.DesktopArrived += OnDesktopArrived;
_desktopArrivalDetector.OnTraceEvent = (decision, reason, context) =>
EmitTraceEvent("DesktopArrivalDetector", decision, reason, context);
_desktopArrivalDetector.Start();
// Agent max lifetime safety net — prevents zombie agents
@@ -129,20 +129,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
});
// Trigger self-destruct or reboot if configured
if (_configuration.SelfDestructOnComplete || _configuration.RebootOnComplete)
{
Task.Run(() => HandleEnrollmentComplete(enrollmentSucceeded));
return; // Don't continue with normal event processing
}
// No self-destruct/reboot — still upload diagnostics and optionally show summary
Task.Run(async () =>
{
await UploadDiagnosticsWithTimelineEvents(enrollmentSucceeded);
if (_configuration.ShowEnrollmentSummary)
LaunchEnrollmentSummaryDialog();
});
// Always run the full completion sequence (analyzers, diagnostics, summary, exit).
// HandleEnrollmentComplete conditionally self-destructs or reboots based on config.
Task.Run(() => HandleEnrollmentComplete(enrollmentSucceeded));
return; // Don't continue with normal event processing
}
// Immediate upload for:
@@ -162,6 +152,41 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
}
/// <summary>
/// Emits a Trace-severity event from any component for agent decision auditing.
/// Always logged locally; only sent to the backend when SendTraceEvents is enabled.
/// </summary>
private void EmitTraceEvent(string source, string decision, string reason, Dictionary<string, object> context = null)
{
_logger.Info($"[TRACE] {source}: {decision} — {reason}");
if (!_configuration.SendTraceEvents)
return;
var data = new Dictionary<string, object>
{
{ "decision", decision },
{ "reason", reason }
};
if (context != null)
{
foreach (var kvp in context)
data[kvp.Key] = kvp.Value;
}
EmitEvent(new EnrollmentEvent
{
SessionId = _configuration.SessionId,
TenantId = _configuration.TenantId,
EventType = "agent_trace",
Severity = EventSeverity.Trace,
Source = source,
Phase = EnrollmentPhase.Unknown,
Message = $"{decision}: {reason}",
Data = data
});
}
/// <summary>
/// Called when new events are available in the spool (FileSystemWatcher detected new files)
/// Uses debouncing to batch events before uploading
@@ -424,7 +449,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
{
try
{
_logger.Info("===== ENROLLMENT COMPLETE - Starting Self-Destruct Sequence =====");
_logger.Info("===== ENROLLMENT COMPLETE - Starting shutdown sequence =====");
// Step 1: Stop all event collectors
_logger.Info("Stopping event collectors...");
@@ -487,7 +512,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
Process.Start(psi);
}
_logger.Info("Self-destruct sequence initiated. Agent will now exit.");
_logger.Info("Shutdown sequence complete. Agent will now exit.");
// Exit the application
Environment.Exit(0);
@@ -583,9 +608,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
return;
}
// Create temp folder for the dialog (survives agent self-destruct)
var tempDir = Path.Combine(Path.GetTempPath(),
"AutopilotMonitor-Summary-" + Guid.NewGuid().ToString("N").Substring(0, 8));
// Create temp folder for the dialog (survives agent self-destruct).
// Use ProgramData instead of Path.GetTempPath() because the agent runs as SYSTEM
// and SYSTEM's temp (C:\WINDOWS\SystemTemp) is not accessible to the user session.
var tempDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"AutopilotMonitor",
"Summary-" + Guid.NewGuid().ToString("N").Substring(0, 8));
Directory.CreateDirectory(tempDir);
// Copy dialog EXE + Newtonsoft.Json.dll + final-status.json to temp
@@ -432,12 +432,40 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_logger.Info($"EnrollmentTracker: {message}");
}
private void HandleImeSessionChange(string changeType)
{
_emitEvent(new EnrollmentEvent
{
SessionId = _sessionId,
TenantId = _tenantId,
EventType = "ime_session_change",
Severity = EventSeverity.Debug,
Source = "ImeLogTracker",
Phase = EnrollmentPhase.Unknown,
Message = $"IME session change: {changeType}",
Data = new Dictionary<string, object> { { "changeType", changeType ?? "" } }
});
}
private void HandleUserSessionCompleted()
{
_logger.Info("EnrollmentTracker: User session completed (detected from IME log)");
_stateData.ImePatternSeenUtc = DateTime.UtcNow;
RecordSignal("ime_pattern");
// Emit timeline event so admins can see exactly when IME finished its user part
EmitImeTrackerEvent(new EnrollmentEvent
{
SessionId = _sessionId,
TenantId = _tenantId,
EventType = "ime_user_session_completed",
Severity = EventSeverity.Info,
Source = "ImeLogTracker",
Phase = EnrollmentPhase.Unknown,
Message = "IME user session completed",
Data = new Dictionary<string, object> { { "detectedAt", DateTime.UtcNow.ToString("o") } }
});
// User session completed successfully — cancel any pending ESP failure
CancelPendingEspFailure();
@@ -179,6 +179,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_imeLogTracker.OnPoliciesDiscovered = HandlePoliciesDiscovered;
_imeLogTracker.OnAllAppsCompleted = HandleAllAppsCompleted;
_imeLogTracker.OnUserSessionCompleted = HandleUserSessionCompleted;
_imeLogTracker.OnImeSessionChange = HandleImeSessionChange;
_imeLogTracker.OnDoTelemetryReceived = HandleDoTelemetryReceived;
_imeLogTracker.OnScriptCompleted = HandleScriptCompleted;
@@ -138,7 +138,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
break;
case "imesessionchange":
_logger.Debug($"IME session change: {match.Groups["change"]?.Value}");
var sessionChange = match.Groups["change"]?.Value;
_logger.Debug($"IME session change: {sessionChange}");
OnImeSessionChange?.Invoke(sessionChange);
break;
case "espphasedetected":
@@ -112,6 +112,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
public Action<string> OnPoliciesDiscovered { get; set; }
public Action OnAllAppsCompleted { get; set; }
public Action OnUserSessionCompleted { get; set; }
public Action<string> OnImeSessionChange { get; set; }
public Action<AppPackageState> OnDoTelemetryReceived { get; set; }
public Action<ScriptExecutionState> OnScriptCompleted { get; set; }
@@ -1 +1 @@
289
292
@@ -66,9 +66,18 @@ namespace AutopilotMonitor.SummaryDialog
var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
var exeDir = Path.GetDirectoryName(exePath);
// Only self-cleanup if running from a temp folder
// Only self-cleanup if running from a known staging folder
// (agent copies the dialog to ProgramData\AutopilotMonitor\Summary-* or legacy TEMP)
if (exeDir == null)
return;
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
var agentStagingRoot = Path.Combine(programData, "AutopilotMonitor");
var tempRoot = Path.GetTempPath().TrimEnd('\\');
if (exeDir == null || !exeDir.StartsWith(tempRoot, StringComparison.OrdinalIgnoreCase))
var isStagedCopy = exeDir.StartsWith(agentStagingRoot, StringComparison.OrdinalIgnoreCase)
|| exeDir.StartsWith(tempRoot, StringComparison.OrdinalIgnoreCase);
if (!isStagedCopy)
return;
var pid = Process.GetCurrentProcess().Id;