Browse Source

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
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
c46602d20d
  1. 35
      ILSpy/App.axaml.cs
  2. 112
      ILSpy/AppEnv/StartupErrorWindow.cs

35
ILSpy/App.axaml.cs

@ -78,17 +78,40 @@ namespace ILSpy @@ -78,17 +78,40 @@ namespace ILSpy
StartupExceptions.Items.Add(new ExceptionData(ex));
}
if (Composition?.GetExport<SettingsService>() is { } settingsService)
try
{
if (Composition?.GetExport<SettingsService>() 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<MainWindow>()
?? new MainWindow();
MainWindow? mainWindow = null;
try
{
AppLog.Mark("MainWindow about to be resolved from MEF");
mainWindow = Composition?.GetExport<MainWindow>();
}
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

112
ILSpy/AppEnv/StartupErrorWindow.cs

@ -0,0 +1,112 @@ @@ -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
{
/// <summary>
/// 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 <see cref="GlobalExceptionHandler"/> could surface a dialog.
/// </summary>
internal sealed class StartupErrorWindow : Window
{
public StartupErrorWindow(IList<ExceptionData> 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);
}
}
}
Loading…
Cancel
Save