Skip to content

Commit 2d73a24

Browse files
committed
feat(hosts): add global exception handling to prevent crashes
Avalonia Host: - Add AppDomain.UnhandledException handler - Add TaskScheduler.UnobservedTaskException handler (SetObserved) - Add Dispatcher.UIThread.UnhandledException handler (Handled=true) - Initialize logger before registering exception handlers Blazor Host (MAUI): - Add AppDomain.UnhandledException handler - Add TaskScheduler.UnobservedTaskException handler - Add Android-specific AndroidEnvironment.UnhandledExceptionRaiser Fix module unload crash: - AvaloniaNavigationService.ClearModuleCache now notifies Shell to clear current view before AssemblyLoadContext.Unload() is called
1 parent cffd4dc commit 2d73a24

File tree

4 files changed

+153
-11
lines changed

4 files changed

+153
-11
lines changed

src/Hosts/Modulus.Host.Avalonia/App.axaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public partial class App : Application
6262
public override void Initialize()
6363
{
6464
AvaloniaXamlLoader.Load(this);
65+
66+
// Register UI thread exception handler after Avalonia is initialized
67+
Program.RegisterDispatcherExceptionHandler();
68+
6569
#if DEBUG
6670
this.AttachDeveloperTools();
6771
#endif
Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,97 @@
11
using Avalonia;
2+
using Avalonia.Threading;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Logging;
5+
using Modulus.Core.Logging;
26
using System;
7+
using System.Threading.Tasks;
38

49
namespace Modulus.Host.Avalonia;
510

611
class Program
712
{
13+
private static ILogger _logger = null!;
14+
815
// Initialization code. Don't use any Avalonia, third-party APIs or any
916
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
1017
// yet and stuff might break.
1118
[STAThread]
12-
public static void Main(string[] args) => BuildAvaloniaApp()
13-
.StartWithClassicDesktopLifetime(args);
19+
public static void Main(string[] args)
20+
{
21+
// Initialize logger first - Serilog doesn't depend on Avalonia
22+
var emptyConfig = new ConfigurationBuilder().Build();
23+
var loggerFactory = ModulusLogging.CreateLoggerFactory(emptyConfig, "AvaloniaApp");
24+
_logger = loggerFactory.CreateLogger<Program>();
25+
26+
// Setup global exception handlers (logger is now ready)
27+
SetupGlobalExceptionHandlers();
28+
_logger.LogInformation("Global exception handlers initialized.");
29+
30+
try
31+
{
32+
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
33+
}
34+
catch (Exception ex)
35+
{
36+
_logger.LogCritical(ex, "Application startup failed: {Message}", ex.Message);
37+
throw;
38+
}
39+
}
1440

1541
// Avalonia configuration, don't remove; also used by visual designer.
1642
public static AppBuilder BuildAvaloniaApp()
1743
=> AppBuilder.Configure<App>()
1844
.UsePlatformDetect()
1945
.WithInterFont()
2046
.LogToTrace();
47+
48+
private static void SetupGlobalExceptionHandlers()
49+
{
50+
// Handle all unhandled exceptions in the AppDomain
51+
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
52+
53+
// Handle unobserved task exceptions
54+
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
55+
}
56+
57+
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
58+
{
59+
var exception = e.ExceptionObject as Exception;
60+
61+
if (e.IsTerminating)
62+
{
63+
_logger.LogCritical(exception, "Fatal unhandled exception - application will terminate: {Message}",
64+
exception?.Message ?? "Unknown error");
65+
}
66+
else
67+
{
68+
_logger.LogError(exception, "Unhandled exception caught: {Message}",
69+
exception?.Message ?? "Unknown error");
70+
}
71+
}
72+
73+
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
74+
{
75+
// Mark as observed to prevent process termination
76+
e.SetObserved();
77+
78+
_logger.LogError(e.Exception, "Unobserved task exception: {Message}", e.Exception.Message);
79+
}
80+
81+
/// <summary>
82+
/// Called by App to register the Avalonia Dispatcher exception handler after Avalonia is initialized.
83+
/// </summary>
84+
internal static void RegisterDispatcherExceptionHandler()
85+
{
86+
Dispatcher.UIThread.UnhandledException += OnDispatcherUnhandledException;
87+
}
88+
89+
private static void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
90+
{
91+
_logger.LogError(e.Exception, "UI thread exception: {Message}", e.Exception.Message);
92+
93+
// Mark as handled to prevent crash - the UI can continue
94+
e.Handled = true;
95+
}
2196
}
2297

src/Hosts/Modulus.Host.Avalonia/Services/AvaloniaNavigationService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ public void ClearModuleCache(string moduleId)
377377
_singletonViews.TryRemove(key, out _);
378378
}
379379

380-
// If current navigation is from this module, clear it
380+
// If current navigation is from this module, navigate away to clear the view
381381
if (_currentViewModel != null && _runtimeContext.TryGetModuleHandle(moduleId, out var currentHandle) && currentHandle != null)
382382
{
383383
var currentAssembly = _currentViewModel.GetType().Assembly;
@@ -386,6 +386,9 @@ public void ClearModuleCache(string moduleId)
386386
_currentViewModel = null;
387387
_currentView = null;
388388
_currentNavigationKey = null;
389+
390+
// Notify shell to clear the current view before module unload
391+
OnViewChanged?.Invoke(null, string.Empty);
389392
}
390393
}
391394
}

src/Hosts/Modulus.Host.Blazor/MauiProgram.cs

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
using Modulus.UI.Abstractions;
1616
using UiMenuItem = Modulus.UI.Abstractions.MenuItem;
1717

18+
#if ANDROID
19+
using Android.Runtime;
20+
#endif
21+
1822
namespace Modulus.Host.Blazor;
1923

2024
[DependsOn()]
@@ -56,10 +60,27 @@ public override Task OnApplicationInitializationAsync(IModuleInitializationConte
5660

5761
public static class MauiProgram
5862
{
63+
private static ILogger _logger = null!;
64+
5965
public static void Main(string[] args) {} // Dummy entry point for net10.0 target without MAUI
6066

6167
public static MauiApp CreateMauiApp()
6268
{
69+
// Configuration (needed for logger)
70+
var configuration = new ConfigurationBuilder()
71+
.SetBasePath(AppContext.BaseDirectory)
72+
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
73+
.AddEnvironmentVariables()
74+
.Build();
75+
76+
// Initialize logger first
77+
var loggerFactory = ModulusLogging.CreateLoggerFactory(configuration, HostType.Blazor);
78+
_logger = loggerFactory.CreateLogger<MauiApp>();
79+
80+
// Setup global exception handlers (logger is now ready)
81+
SetupGlobalExceptionHandlers();
82+
_logger.LogInformation("Global exception handlers initialized.");
83+
6384
var builder = MauiApp.CreateBuilder();
6485
builder
6586
.UseMauiApp<App>()
@@ -68,14 +89,6 @@ public static MauiApp CreateMauiApp()
6889
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
6990
});
7091

71-
// Configuration
72-
var configuration = new ConfigurationBuilder()
73-
.SetBasePath(AppContext.BaseDirectory)
74-
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
75-
.AddEnvironmentVariables()
76-
.Build();
77-
78-
var loggerFactory = ModulusLogging.CreateLoggerFactory(configuration, HostType.Blazor);
7992
ModulusLogging.AddLoggerFactory(builder.Services, loggerFactory);
8093

8194
// Module Providers - load from Modules/ directory relative to executable
@@ -129,4 +142,51 @@ public static MauiApp CreateMauiApp()
129142
return app;
130143
}
131144

145+
private static void SetupGlobalExceptionHandlers()
146+
{
147+
// Handle all unhandled exceptions in the AppDomain
148+
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
149+
150+
// Handle unobserved task exceptions
151+
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
152+
153+
#if ANDROID
154+
// Android-specific unhandled exception handler
155+
AndroidEnvironment.UnhandledExceptionRaiser += OnAndroidUnhandledException;
156+
#endif
157+
}
158+
159+
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
160+
{
161+
var exception = e.ExceptionObject as Exception;
162+
163+
if (e.IsTerminating)
164+
{
165+
_logger.LogCritical(exception, "Fatal unhandled exception - application will terminate: {Message}",
166+
exception?.Message ?? "Unknown error");
167+
}
168+
else
169+
{
170+
_logger.LogError(exception, "Unhandled exception caught: {Message}",
171+
exception?.Message ?? "Unknown error");
172+
}
173+
}
174+
175+
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
176+
{
177+
// Mark as observed to prevent process termination
178+
e.SetObserved();
179+
180+
_logger.LogError(e.Exception, "Unobserved task exception: {Message}", e.Exception.Message);
181+
}
182+
183+
#if ANDROID
184+
private static void OnAndroidUnhandledException(object? sender, RaiseThrowableEventArgs e)
185+
{
186+
_logger.LogError(e.Exception, "Android unhandled exception: {Message}", e.Exception.Message);
187+
188+
// Set handled to prevent crash where possible
189+
e.Handled = true;
190+
}
191+
#endif
132192
}

0 commit comments

Comments
 (0)