diff --git a/.gitignore b/.gitignore index 96f9b072..878e17cc 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,6 @@ tmpclaude-*-cwd # Deployment scripts (local only) scripts/Deployment/build_and_upload_debug_agent_build.ps1 scripts/Deployment/build_and_upload_release_agent_build.ps1 + + +.dotnet-home/.dotnet/* diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 47eb669b..4dab36bb 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -316 +317 diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs index be2cc212..5cbe9491 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs +++ b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Text; using System.Windows; namespace AutopilotMonitor.SummaryDialog @@ -13,6 +14,8 @@ namespace AutopilotMonitor.SummaryDialog internal static bool? ForceTheme { get; private set; } // true=dark, false=light, null=auto private static string _logFile; + private static string _tempLogFile; // error log in %TEMP% (survives self-cleanup) + private static bool _hadErrors; public App() { @@ -23,21 +26,61 @@ namespace AutopilotMonitor.SummaryDialog var exeDir = Path.GetDirectoryName( System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "."; _logFile = Path.Combine(exeDir, "SummaryDialog.log"); + + // Error log in user's temp folder — survives self-cleanup for troubleshooting + try { _tempLogFile = Path.Combine(Path.GetTempPath(), "SummaryDialog-error.log"); } + catch { /* temp path unavailable */ } + Log("App constructor — process started"); + LogDiagnostics(); + } + catch (Exception ex) + { + LogError($"App constructor failed: {ex}"); } - catch { } AppDomain.CurrentDomain.UnhandledException += (s, e) => { - Log($"UNHANDLED EXCEPTION: {e.ExceptionObject}"); + LogError($"UNHANDLED EXCEPTION: {e.ExceptionObject}"); }; DispatcherUnhandledException += (s, e) => { - Log($"DISPATCHER EXCEPTION: {e.Exception}"); + LogError($"DISPATCHER EXCEPTION: {e.Exception}"); + e.Handled = true; // prevent uncontrolled crash, ensure OnExit runs + try { Shutdown(1); } catch { } }; } + private void LogDiagnostics() + { + try + { + var proc = Process.GetCurrentProcess(); + Log($"PID={proc.Id} SessionId={proc.SessionId}"); + + // Session 0 = SYSTEM session — window will NOT be visible to the user + if (proc.SessionId == 0) + LogError("CRITICAL: Process is in Session 0 (SYSTEM) — window will NOT be visible!"); + + Log($"User: {Environment.UserDomainName}\\{Environment.UserName}"); + Log($"CommandLine: {Environment.CommandLine}"); + Log($"ExeDir: {Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)}"); + Log($"Temp: {Path.GetTempPath()}"); + Log($"Is64Bit: {Environment.Is64BitProcess} OS: {Environment.OSVersion}"); + + // Check if critical dependency exists + var exeDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var jsonDll = Path.Combine(exeDir ?? ".", "Newtonsoft.Json.dll"); + if (!File.Exists(jsonDll)) + LogError($"Newtonsoft.Json.dll NOT FOUND at {jsonDll}"); + } + catch (Exception ex) + { + LogError($"LogDiagnostics failed: {ex.Message}"); + } + } + protected override void OnStartup(StartupEventArgs e) { Log($"OnStartup — args: {string.Join(" ", e.Args)}"); @@ -49,6 +92,7 @@ namespace AutopilotMonitor.SummaryDialog { Log("OnExit — shutting down"); base.OnExit(e); + CleanupTempErrorLog(); SelfCleanup(); } @@ -84,8 +128,9 @@ namespace AutopilotMonitor.SummaryDialog } /// - /// Self-cleanup: delete the temp folder this EXE was launched from. - /// Uses a cmd.exe script that waits for our process to exit, then deletes the folder. + /// Self-cleanup: delete the staging folder this EXE was launched from. + /// Uses PowerShell Wait-Process to cleanly wait for our process to exit, + /// then deletes the folder. No polling loops or race conditions. /// private void SelfCleanup() { @@ -110,25 +155,46 @@ namespace AutopilotMonitor.SummaryDialog return; var pid = Process.GetCurrentProcess().Id; - // Wait for our process to exit, then delete the temp folder - var script = $@"/c tasklist /fi ""PID eq {pid}"" 2>nul | find ""{pid}"" >nul && (ping 127.0.0.1 -n 3 >nul) & rd /s /q ""{exeDir}"""; + + // PowerShell Wait-Process: waits for our process to actually exit (no polling), + // then deletes the staging folder. 60s timeout as safety net. + var psScript = $"Wait-Process -Id {pid} -Timeout 60 -ErrorAction SilentlyContinue; " + + $"Start-Sleep -Seconds 1; " + + $"Remove-Item -LiteralPath '{exeDir}' -Recurse -Force -ErrorAction SilentlyContinue"; + var encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(psScript)); + + Log($"SelfCleanup: spawning PowerShell to delete '{exeDir}' after PID {pid} exits"); var psi = new ProcessStartInfo { - FileName = "cmd.exe", - Arguments = script, + FileName = "powershell.exe", + Arguments = $"-NoProfile -NonInteractive -EncodedCommand {encoded}", WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, UseShellExecute = false }; Process.Start(psi); } - catch + catch (Exception ex) { + Log($"SelfCleanup failed: {ex.Message}"); // Best-effort cleanup, ignore errors } } + /// + /// Delete the temp error log if no errors occurred (no traces on success). + /// + private void CleanupTempErrorLog() + { + try + { + if (!_hadErrors && _tempLogFile != null && File.Exists(_tempLogFile)) + File.Delete(_tempLogFile); + } + catch { } + } + internal static void Log(string message) { try @@ -139,5 +205,22 @@ namespace AutopilotMonitor.SummaryDialog } catch { } } + + /// + /// Log an error to both the main log and the temp error log. + /// The temp error log survives self-cleanup for post-mortem troubleshooting. + /// + internal static void LogError(string message) + { + _hadErrors = true; + Log(message); + try + { + if (_tempLogFile != null) + File.AppendAllText(_tempLogFile, + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}"); + } + catch { } + } } } diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs index 5abd109d..7692a579 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs +++ b/src/Agent/AutopilotMonitor.SummaryDialog/MainWindow.xaml.cs @@ -26,28 +26,41 @@ namespace AutopilotMonitor.SummaryDialog public MainWindow() { + App.Log("MainWindow constructor — InitializeComponent"); InitializeComponent(); + App.Log("MainWindow constructor — done"); } private async void Window_Loaded(object sender, RoutedEventArgs e) { - _isDarkTheme = App.ForceTheme ?? DetectDarkTheme(); - if (_isDarkTheme) - ApplyDarkTheme(); - - ApplyScrollBarStyle(); - ApplyContentClip(); - ApplyTaskbarIcon(); - - LoadStatusData(); - RenderOutcome(); - RenderAppList(); - StartCountdown(); - - // Load branding image (async, best-effort) - if (!string.IsNullOrEmpty(App.BrandingImageUrl)) + App.Log("Window_Loaded — start"); + try { - await LoadBrandingImageAsync(); + _isDarkTheme = App.ForceTheme ?? DetectDarkTheme(); + App.Log($"Theme: isDark={_isDarkTheme} (forced={App.ForceTheme})"); + if (_isDarkTheme) + ApplyDarkTheme(); + + ApplyScrollBarStyle(); + ApplyContentClip(); + ApplyTaskbarIcon(); + + LoadStatusData(); + RenderOutcome(); + RenderAppList(); + StartCountdown(); + + // Load branding image (async, best-effort) + if (!string.IsNullOrEmpty(App.BrandingImageUrl)) + { + await LoadBrandingImageAsync(); + } + + App.Log("Window_Loaded — complete, window should be visible"); + } + catch (Exception ex) + { + App.LogError($"Window_Loaded FAILED: {ex}"); } } @@ -59,10 +72,16 @@ namespace AutopilotMonitor.SummaryDialog { var json = File.ReadAllText(App.StatusFilePath); _status = JsonConvert.DeserializeObject(json); + App.Log($"LoadStatusData: outcome={_status?.Outcome} apps={_status?.AppSummary?.TotalApps}"); + } + else + { + App.Log($"LoadStatusData: status file missing or empty (path={App.StatusFilePath})"); } } - catch + catch (Exception ex) { + App.LogError($"LoadStatusData FAILED: {ex.Message}"); _status = null; } } @@ -277,9 +296,9 @@ namespace AutopilotMonitor.SummaryDialog BrandingBanner.Visibility = Visibility.Visible; } } - catch + catch (Exception ex) { - // Best-effort: skip banner on any error + App.Log($"LoadBrandingImage: skipped ({ex.Message})"); BrandingBanner.Visibility = Visibility.Collapsed; } } @@ -479,11 +498,14 @@ namespace AutopilotMonitor.SummaryDialog @"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize")) { var value = key?.GetValue("AppsUseLightTheme"); - return value is int i && i == 0; + var isDark = value is int i && i == 0; + App.Log($"DetectDarkTheme: AppsUseLightTheme={value} → isDark={isDark}"); + return isDark; } } - catch + catch (Exception ex) { + App.Log($"DetectDarkTheme: registry read failed ({ex.Message}), defaulting to light"); return false; } }