From c46602d20d74a484cfbbce6e9490421e80c181f9 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 23 May 2026 21:24:14 +0200 Subject: [PATCH] Show startup exceptions in a dedicated error window If anything thrown by App.OnFrameworkInitializationCompleted propagated past it -- a CompositionFailedException from an unresolvable plugin dependency, say -- the process died before the dispatcher pump started, so the AppDomain.UnhandledException dialog wired by GlobalExceptionHandler never had a chance to surface. The user saw a silent exit; the only trace was a stderr stack on terminal launches and nothing at all when clicking the icon. Wrap the critical resolution paths so anything that goes wrong lands in StartupExceptions, and on any failure swap the MainWindow for a StartupErrorWindow that displays the captured text. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/App.axaml.cs | 35 +++++++-- ILSpy/AppEnv/StartupErrorWindow.cs | 112 +++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 ILSpy/AppEnv/StartupErrorWindow.cs diff --git a/ILSpy/App.axaml.cs b/ILSpy/App.axaml.cs index f2b3d5c70..64edee8e9 100644 --- a/ILSpy/App.axaml.cs +++ b/ILSpy/App.axaml.cs @@ -78,17 +78,40 @@ namespace ILSpy StartupExceptions.Items.Add(new ExceptionData(ex)); } - if (Composition?.GetExport() is { } settingsService) + try + { + if (Composition?.GetExport() is { } settingsService) + { + ThemeManager.Current.Attach(settingsService.SessionSettings); + ApplyCulture(settingsService.SessionSettings.CurrentCulture); + } + } + catch (Exception ex) { - ThemeManager.Current.Attach(settingsService.SessionSettings); - ApplyCulture(settingsService.SessionSettings.CurrentCulture); + StartupExceptions.Items.Add(new ExceptionData(ex)); } if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - AppLog.Mark("MainWindow about to be resolved from MEF"); - desktop.MainWindow = Composition?.GetExport() - ?? new MainWindow(); + MainWindow? mainWindow = null; + try + { + AppLog.Mark("MainWindow about to be resolved from MEF"); + mainWindow = Composition?.GetExport(); + } + catch (Exception ex) + { + StartupExceptions.Items.Add(new ExceptionData(ex)); + } + + // Without this fallback, anything thrown by MEF resolution above propagates out of + // OnFrameworkInitializationCompleted before the dispatcher pump starts -- so the + // AppDomain.UnhandledException dialog posted by GlobalExceptionHandler never gets + // a chance to run, and the user sees a silent exit. Hand the user a window that + // shows the captured exceptions instead. + desktop.MainWindow = StartupExceptions.Items.Count > 0 + ? new StartupErrorWindow(StartupExceptions.Items) + : mainWindow ?? new MainWindow(); AppLog.Mark("MainWindow assigned to desktop.MainWindow"); desktop.Exit += (_, _) => { try diff --git a/ILSpy/AppEnv/StartupErrorWindow.cs b/ILSpy/AppEnv/StartupErrorWindow.cs new file mode 100644 index 000000000..07ff581a4 --- /dev/null +++ b/ILSpy/AppEnv/StartupErrorWindow.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; + +using global::Avalonia; +using global::Avalonia.Controls; +using global::Avalonia.Input; +using global::Avalonia.Input.Platform; +using global::Avalonia.Layout; +using global::Avalonia.Media; + +using CommunityToolkit.Mvvm.Input; + +namespace ILSpy.AppEnv +{ + /// + /// Shown in place of the main window when startup fails before the dispatcher pump begins. + /// Without this, a CompositionFailedException (e.g. an unresolvable plugin dependency) + /// would propagate out of App.OnFrameworkInitializationCompleted and the process would + /// die before could surface a dialog. + /// + internal sealed class StartupErrorWindow : Window + { + public StartupErrorWindow(IList exceptions) + { + Title = "ILSpy -- startup failed"; + Width = 720; + Height = 480; + WindowStartupLocation = WindowStartupLocation.CenterScreen; + + var clipboardText = StartupExceptions.Format(); + var details = new TextBox { + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.NoWrap, + FontFamily = new FontFamily("Consolas, Menlo, Monospace"), + FontSize = 12, + Text = clipboardText, + }; + + var summary = new TextBlock { + Margin = new Thickness(0, 0, 0, 8), + FontWeight = FontWeight.Bold, + TextWrapping = TextWrapping.Wrap, + Text = exceptions.Count == 1 + ? "ILSpy could not start because of a startup error." + : $"ILSpy could not start: {exceptions.Count} startup errors occurred.", + }; + + var copy = new Button { Content = "Copy" }; + copy.Click += (_, _) => CopyToClipboard(clipboardText); + + var exit = new Button { Content = "Exit", IsDefault = true }; + exit.Click += (_, _) => Close(); + + var buttons = new StackPanel { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Margin = new Thickness(0, 8, 0, 0), + Spacing = 8, + Children = { copy, exit }, + }; + + var grid = new Grid { + Margin = new Thickness(12), + RowDefinitions = new RowDefinitions("Auto,*,Auto"), + }; + Grid.SetRow(summary, 0); + Grid.SetRow(details, 1); + Grid.SetRow(buttons, 2); + grid.Children.Add(summary); + grid.Children.Add(details); + grid.Children.Add(buttons); + + Content = grid; + + KeyBindings.Add(new KeyBinding { + Gesture = new KeyGesture(Key.C, KeyModifiers.Control), + Command = new RelayCommand( + () => CopyToClipboard(clipboardText), + () => string.IsNullOrEmpty(details.SelectedText)), + }); + KeyBindings.Add(new KeyBinding { + Gesture = new KeyGesture(Key.Escape), + Command = new RelayCommand(Close), + }); + } + + void CopyToClipboard(string text) + { + var clipboard = TopLevel.GetTopLevel(this)?.Clipboard; + if (clipboard != null) + _ = clipboard.SetTextAsync(text); + } + } +}