diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/PinnedShortcutsHelper.cs b/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/PinnedShortcutsHelper.cs index 0f19956eb7a6..10529c17d62e 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/PinnedShortcutsHelper.cs +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/Helpers/PinnedShortcutsHelper.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; +using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using ShortcutGuide.Models; @@ -32,16 +33,27 @@ public static void UpdatePinnedShortcuts(string appName, ShortcutEntry shortcutE list.Add(shortcutEntry); } + // Persist on a best-effort basis. The in-memory pinned list is the source of truth + // for the rest of the session; failing to write should not crash the overlay + // (Pin/Unpin runs from a synchronous UI handler). Save(); PinnedShortcutsChanged?.Invoke(null, appName); } public static void Save() { - string serialized = JsonSerializer.Serialize(App.PinnedShortcuts); - - string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json"); - File.WriteAllText(pinnedPath, serialized); + try + { + string serialized = JsonSerializer.Serialize(App.PinnedShortcuts); + string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json"); + File.WriteAllText(pinnedPath, serialized); + } + catch (Exception ex) when (ex is IOException + or UnauthorizedAccessException + or JsonException) + { + Logger.LogError("Failed to persist Shortcut Guide pinned shortcuts; keeping in-memory state.", ex); + } } } } diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs index 684bef00df33..c3ca5fd9d92a 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/App.xaml.cs @@ -2,9 +2,12 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Threading.Tasks; +using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; @@ -31,21 +34,39 @@ public partial class App public App() { this.InitializeComponent(); + + // Register process-wide exception handlers so a stray exception (e.g. an IO failure + // during a fire-and-forget UI handler, or a background Task fault) gets logged + // instead of taking the overlay down with an unhandled access violation in coreclr. + // Without these the runtime tears the process down before our local catches can run. + this.UnhandledException += App_UnhandledException; + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } protected override void OnLaunched(LaunchActivatedEventArgs args) { - this.LoadData(); - MainWindow = new MainWindow(); - TaskBarWindow = new TaskbarWindow(); - MainWindow.Activate(); - MainWindow.Closed += (_, _) => + try { - PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent( - MainWindow.SessionDurationMs, - MainWindow.CloseType)); - TaskBarWindow.Close(); - }; + this.LoadData(); + MainWindow = new MainWindow(); + TaskBarWindow = new TaskbarWindow(); + MainWindow.Activate(); + MainWindow.Closed += (_, _) => + { + PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent( + MainWindow.SessionDurationMs, + MainWindow.CloseType)); + TaskBarWindow.Close(); + }; + } + catch (Exception ex) + { + // Any failure in launch is fatal for this short-lived overlay; log and exit + // cleanly rather than letting WinUI surface a generic crash dialog. + Logger.LogError("Failed to launch Shortcut Guide.", ex); + Environment.Exit(1); + } } private void LoadData() @@ -63,18 +84,53 @@ private void LoadData() PinnedShortcuts = loaded; } } - catch (JsonException) + catch (Exception ex) when (ex is JsonException + or IOException + or UnauthorizedAccessException) { - // Fall back to the empty default if the file is corrupt. + // Fall back to the empty default if the file is corrupt or unreadable. + Logger.LogWarning($"Failed to load pinned shortcuts from '{pinnedPath}'. Falling back to empty list. Reason: {ex.Message}"); } } ShortcutGuideSettings = SettingsRepository.GetInstance(settingsUtils).SettingsConfig; ShortcutGuideProperties = ShortcutGuideSettings.Properties; + try + { #pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances - settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide"); + settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide"); #pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // Persisting the round-tripped settings is best-effort; the in-memory copy is still valid. + Logger.LogWarning($"Failed to persist Shortcut Guide settings on launch. Reason: {ex.Message}"); + } + } + + private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + // Exceptions raised on the UI thread land here. Mark handled so the runtime + // does not terminate the process; the overlay can usually continue. + Logger.LogError("Unhandled UI exception in Shortcut Guide.", e.Exception); + e.Handled = true; + } + + private static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e) + { + // Background-thread exceptions reach here as a last resort; we cannot prevent + // termination when IsTerminating is true, but at least we leave a log trail. + if (e.ExceptionObject is Exception ex) + { + Logger.LogError($"Unhandled background exception in Shortcut Guide (IsTerminating={e.IsTerminating}).", ex); + } + } + + private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + Logger.LogError("Unobserved Task exception in Shortcut Guide.", e.Exception); + e.SetObserved(); } } } diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/MainWindow.xaml.cs b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/MainWindow.xaml.cs index 996a58436867..f7d9a400be4a 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/MainWindow.xaml.cs +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/MainWindow.xaml.cs @@ -237,37 +237,54 @@ private static PathIcon CreatePathIcon(string pathData) private void SetWindowPosition() { - if (!this._hasMovedToRightMonitor) + try { - NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint); - AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) }); - this._hasMovedToRightMonitor = true; - } + if (!this._hasMovedToRightMonitor) + { + NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint); + AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) }); + this._hasMovedToRightMonitor = true; + } - var hwnd = WindowNative.GetWindowHandle(this); - float dpi = DpiHelper.GetDPIScaleForWindow(hwnd); - Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd); + var hwnd = WindowNative.GetWindowHandle(this); + float dpi = DpiHelper.GetDPIScaleForWindow(hwnd); + Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd); - var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value; - var taskbarWindow = App.TaskBarWindow.AppWindow; - bool taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left; - bool taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right; + var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value; - double newHeight = monitorRect.Height / dpi; - if (taskbarOnLeft || taskbarOnRight) - { - newHeight -= taskbarWindow.Size.Height; - } + // App.TaskBarWindow / its AppWindow can briefly be null during the reentrant + // Hide → Activate → BringToFront chain triggered from SelectionChanged. When the + // taskbar window is not currently observable, skip the overlap adjustment instead + // of crashing the overlay (issue #48448). + var taskbarWindow = App.TaskBarWindow?.AppWindow; + bool taskbarOnLeft = false; + bool taskbarOnRight = false; + if (taskbarWindow is not null) + { + taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left; + taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right; + } + + double newHeight = monitorRect.Height / dpi; + if (taskbarWindow is not null && (taskbarOnLeft || taskbarOnRight)) + { + newHeight -= taskbarWindow.Size.Height; + } - MaxHeight = newHeight; - MinHeight = newHeight; - Height = newHeight; + MaxHeight = newHeight; + MinHeight = newHeight; + Height = newHeight; - int xPosition = windowPosition == ShortcutGuideWindowPosition.Right - ? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi) - : (int)monitorRect.X; + int xPosition = windowPosition == ShortcutGuideWindowPosition.Right + ? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi) + : (int)monitorRect.X; - this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height); + this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height); + } + catch (Exception ex) + { + Logger.LogError("Failed to set Shortcut Guide window position; keeping previous layout.", ex); + } } /// @@ -282,25 +299,35 @@ private void WindowSelector_SelectionChanged(NavigationView sender, NavigationVi return; } - this._selectedAppName = selectedItem.Name; - App.CurrentAppName = this._selectedAppName; - this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName); - - App.TaskBarWindow.Hide(); - if (this._shortcutFile is ShortcutFile file) + try { - // Show the taskbar button window only when the selected app exposes the section. - if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("", StringComparison.Ordinal) == true)) + this._selectedAppName = selectedItem.Name; + App.CurrentAppName = this._selectedAppName; + this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName); + + App.TaskBarWindow?.Hide(); + if (this._shortcutFile is ShortcutFile file) { - this._taskBarWindowActivated = true; - App.TaskBarWindow.Activate(); - } + // Show the taskbar button window only when the selected app exposes the section. + if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("", StringComparison.Ordinal) == true)) + { + this._taskBarWindowActivated = true; + App.TaskBarWindow?.Activate(); + } - // Reposition before navigating so the taskbar window does not clip into the main window. - this.SetWindowPosition(); - this.ContentFrame.Navigate( - typeof(ShortcutsPage), - new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName }); + // Reposition before navigating so the taskbar window does not clip into the main window. + this.SetWindowPosition(); + this.ContentFrame.Navigate( + typeof(ShortcutsPage), + new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName }); + } + } + catch (Exception ex) + { + // Guard against exceptions during section navigation so the overlay does not close on the user. + // InitializeNavItemsAsync's catch interprets any exception bubbling out of the initial + // SelectedItem assignment as a fatal init failure and closes the window (issue #48448). + Logger.LogError($"Failed to handle Shortcut Guide section selection '{selectedItem.Name}'.", ex); } } diff --git a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/TaskbarWindow.xaml.cs b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/TaskbarWindow.xaml.cs index f13fcf234a90..90c1640a5de8 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/TaskbarWindow.xaml.cs +++ b/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/TaskbarWindow.xaml.cs @@ -30,54 +30,73 @@ public TaskbarWindow() public void UpdateTasklistButtons() { - // This move ensures the window spawns on the same monitor as the main window - AppWindow.MoveInZOrderAtBottom(); - AppWindow.Move(App.MainWindow.AppWindow.Position); - TasklistButton[] buttons = []; + // Wrap the entire body: this method runs from the ctor and from `Activated`, + // both of which can fire while MainWindow is closing or AppWindow is in a + // transient null state. An exception here used to crash the overlay because + // there was no caller-side try/catch (issue #48441). try { - buttons = TasklistPositions.GetButtons(); - } - catch (Exception ex) - { - Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex); - } + // This move ensures the window spawns on the same monitor as the main window. + // App.MainWindow / its AppWindow can briefly be null during the reentrant + // Hide → Activate → BringToFront chain triggered from SelectionChanged. + var mainAppWindow = App.MainWindow?.AppWindow; + if (mainAppWindow is null) + { + return; + } - if (buttons.Length == 0) - { - AppWindow.Hide(); - return; - } + AppWindow.MoveInZOrderAtBottom(); + AppWindow.Move(mainAppWindow.Position); + TasklistButton[] buttons = []; + try + { + buttons = TasklistPositions.GetButtons(); + } + catch (Exception ex) + { + Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex); + } - float dpi = this.DPI; - double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value; - double windowHeight = 58; - double windowMargin = 8 * dpi; - double windowWidth = windowsLogoColumnWidth; - double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi); - double yPosition = this.WorkArea.Bottom - (windowHeight * dpi); + if (buttons.Length == 0) + { + AppWindow.Hide(); + return; + } - this.KeyHolder.Children.Clear(); + float dpi = this.DPI; + double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value; + double windowHeight = 58; + double windowMargin = 8 * dpi; + double windowWidth = windowsLogoColumnWidth; + double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi); + double yPosition = this.WorkArea.Bottom - (windowHeight * dpi); - foreach (TasklistButton b in buttons) - { - TaskbarIndicator indicator = new() + this.KeyHolder.Children.Clear(); + + foreach (TasklistButton b in buttons) { - Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture), - Height = b.Height / dpi, - Width = b.Width / dpi, - }; + TaskbarIndicator indicator = new() + { + Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture), + Height = b.Height / dpi, + Width = b.Width / dpi, + }; - windowWidth += indicator.Width; + windowWidth += indicator.Width; - this.KeyHolder.Children.Add(indicator); + this.KeyHolder.Children.Add(indicator); - double indicatorPos = (b.X - xPosition) / dpi; - Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth); - } + double indicatorPos = (b.X - xPosition) / dpi; + Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth); + } - this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight); - AppWindow.MoveInZOrderAtTop(); + this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight); + AppWindow.MoveInZOrderAtTop(); + } + catch (Exception ex) + { + Logger.LogError("Failed to update Shortcut Guide taskbar indicator window.", ex); + } } } }