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 5b93e57f..85e78b91 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Security.AccessControl; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using AutopilotMonitor.Agent.Core.Configuration; @@ -608,15 +610,20 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core return; } - // 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. + // Create temp folder for the dialog OUTSIDE the AutopilotMonitor folder. + // The agent's self-destruct deletes ProgramData\AutopilotMonitor entirely, + // so the dialog files must live elsewhere to survive. var tempDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - "AutopilotMonitor", - "Summary-" + Guid.NewGuid().ToString("N").Substring(0, 8)); + "AutopilotMonitor-Summary"); + + // Clean up any leftover folder from a previous run, then create fresh + try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } Directory.CreateDirectory(tempDir); + // Grant the interactive user delete permissions so the dialog can self-cleanup + GrantUserDeletePermission(tempDir); + // Copy dialog EXE + Newtonsoft.Json.dll + final-status.json to temp var tempDialogExe = Path.Combine(tempDir, "AutopilotMonitor.SummaryDialog.exe"); File.Copy(dialogExe, tempDialogExe); @@ -625,6 +632,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core if (File.Exists(jsonDll)) File.Copy(jsonDll, Path.Combine(tempDir, "Newtonsoft.Json.dll")); + var exeConfig = dialogExe + ".config"; + if (File.Exists(exeConfig)) + File.Copy(exeConfig, tempDialogExe + ".config"); + var tempStatusFile = Path.Combine(tempDir, "final-status.json"); File.Copy(statusFile, tempStatusFile); @@ -667,6 +678,31 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core } } + /// + /// Grants the built-in Users group full control on a directory so the user-session + /// SummaryDialog process can delete it during self-cleanup. + /// + private void GrantUserDeletePermission(string directoryPath) + { + try + { + var dirInfo = new DirectoryInfo(directoryPath); + var security = dirInfo.GetAccessControl(); + var usersIdentity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + security.AddAccessRule(new FileSystemAccessRule( + usersIdentity, + FileSystemRights.FullControl, + InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, + PropagationFlags.None, + AccessControlType.Allow)); + dirInfo.SetAccessControl(security); + } + catch (Exception ex) + { + _logger.Debug($"Could not set ACL on summary folder: {ex.Message}"); + } + } + public void Dispose() { _uploadTimer?.Dispose(); diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index cd307095..33a21f83 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -306 +308 diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs index 73edefa3..be2cc212 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs +++ b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs @@ -12,14 +12,42 @@ namespace AutopilotMonitor.SummaryDialog internal static string BrandingImageUrl { get; private set; } internal static bool? ForceTheme { get; private set; } // true=dark, false=light, null=auto + private static string _logFile; + + public App() + { + // Set up crash logging as early as possible to diagnose launch failures + // (e.g. when launched via CreateProcessAsUser from SYSTEM service) + try + { + var exeDir = Path.GetDirectoryName( + System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "."; + _logFile = Path.Combine(exeDir, "SummaryDialog.log"); + Log("App constructor — process started"); + } + catch { } + + AppDomain.CurrentDomain.UnhandledException += (s, e) => + { + Log($"UNHANDLED EXCEPTION: {e.ExceptionObject}"); + }; + + DispatcherUnhandledException += (s, e) => + { + Log($"DISPATCHER EXCEPTION: {e.Exception}"); + }; + } + protected override void OnStartup(StartupEventArgs e) { + Log($"OnStartup — args: {string.Join(" ", e.Args)}"); base.OnStartup(e); ParseArguments(e.Args); } protected override void OnExit(ExitEventArgs e) { + Log("OnExit — shutting down"); base.OnExit(e); SelfCleanup(); } @@ -67,15 +95,16 @@ namespace AutopilotMonitor.SummaryDialog var exeDir = Path.GetDirectoryName(exePath); // 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 summaryStaging = Path.Combine(programData, "AutopilotMonitor-Summary"); + var legacyStaging = Path.Combine(programData, "AutopilotMonitor"); var tempRoot = Path.GetTempPath().TrimEnd('\\'); - var isStagedCopy = exeDir.StartsWith(agentStagingRoot, StringComparison.OrdinalIgnoreCase) + var isStagedCopy = exeDir.StartsWith(summaryStaging, StringComparison.OrdinalIgnoreCase) + || exeDir.StartsWith(legacyStaging, StringComparison.OrdinalIgnoreCase) || exeDir.StartsWith(tempRoot, StringComparison.OrdinalIgnoreCase); if (!isStagedCopy) return; @@ -99,5 +128,16 @@ namespace AutopilotMonitor.SummaryDialog // Best-effort cleanup, ignore errors } } + + internal static void Log(string message) + { + try + { + if (_logFile != null) + File.AppendAllText(_logFile, + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}"); + } + catch { } + } } } diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/AutopilotMonitor.SummaryDialog.csproj b/src/Agent/AutopilotMonitor.SummaryDialog/AutopilotMonitor.SummaryDialog.csproj index f3facfc2..e592db77 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/AutopilotMonitor.SummaryDialog.csproj +++ b/src/Agent/AutopilotMonitor.SummaryDialog/AutopilotMonitor.SummaryDialog.csproj @@ -7,6 +7,7 @@ true AutopilotMonitor.SummaryDialog AutopilotMonitor.SummaryDialog + app.ico diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/app.ico b/src/Agent/AutopilotMonitor.SummaryDialog/app.ico new file mode 100644 index 00000000..51f34476 Binary files /dev/null and b/src/Agent/AutopilotMonitor.SummaryDialog/app.ico differ