diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs index c8152a3a..1c286256 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs @@ -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; /// @@ -38,6 +40,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors /// public event EventHandler DesktopArrived; + /// + /// Optional callback for trace events (decision, reason, context). + /// Wired by MonitoringService to emit agent_trace events to the backend. + /// + public Action> 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 { { "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 { { "pid", proc.Id }, { "session", proc.SessionId }, { "owner", owner } }); + } + catch { } + // Stop polling _pollingTimer?.Dispose(); _pollingTimer = null; diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs index 046408a9..645090f4 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs @@ -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 diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs index 867c3cb0..5b93e57f 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs @@ -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 } } + /// + /// Emits a Trace-severity event from any component for agent decision auditing. + /// Always logged locally; only sent to the backend when SendTraceEvents is enabled. + /// + private void EmitTraceEvent(string source, string decision, string reason, Dictionary context = null) + { + _logger.Info($"[TRACE] {source}: {decision} — {reason}"); + + if (!_configuration.SendTraceEvents) + return; + + var data = new Dictionary + { + { "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 + }); + } + /// /// 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 diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs index 78db9cbe..44c4a4c9 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs @@ -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 { { "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 { { "detectedAt", DateTime.UtcNow.ToString("o") } } + }); + // User session completed successfully — cancel any pending ESP failure CancelPendingEspFailure(); diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs index 67ec7910..e184dd76 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs @@ -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; diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs index f14ac336..dec47f23 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs @@ -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": diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs index f67d7559..a038f38e 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs @@ -112,6 +112,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking public Action OnPoliciesDiscovered { get; set; } public Action OnAllAppsCompleted { get; set; } public Action OnUserSessionCompleted { get; set; } + public Action OnImeSessionChange { get; set; } public Action OnDoTelemetryReceived { get; set; } public Action OnScriptCompleted { get; set; } diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 336dd5e3..f20bd4ef 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -289 +292 diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs index eb13dc2c..73edefa3 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs +++ b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs @@ -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;