Skip to content

Commit 386de0f

Browse files
committed
Improved crash handling
- Show a dialog with a button to try restarting the program (instead of simply disappearing from the tray) - Catch errors during initialization - Fixed unhandled exceptions on the UI thread not being logged
1 parent 7de35d8 commit 386de0f

4 files changed

Lines changed: 118 additions & 15 deletions

File tree

AutoAudioSwitcher/Program.cs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Copyright (c) Max Kagamine
22
// Licensed under the Apache License, Version 2.0
33

4+
using AutoAudioSwitcher.Properties;
45
using Microsoft.Extensions.Configuration;
56
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Win32;
78
using Serilog;
89
using Serilog.Core;
910
using Serilog.Events;
1011
using Serilog.Templates;
12+
using System.Diagnostics;
1113
using System.Reactive;
1214
using System.Reactive.Linq;
1315

@@ -16,7 +18,13 @@ namespace AutoAudioSwitcher;
1618
internal sealed class Program
1719
{
1820
private static readonly TimeSpan WindowsAutomaticDefaultDeviceChangeThreshold = TimeSpan.FromSeconds(2);
21+
22+
private const string LogsDirectory = "logs";
23+
1924
private static readonly LoggingLevelSwitch levelSwitch = new(LogEventLevel.Error);
25+
private static ServiceProvider? provider;
26+
private static ILogger? logger;
27+
private static Mutex? singleInstanceMutex;
2028

2129
private static ServiceProvider ConfigureServices()
2230
{
@@ -39,7 +47,7 @@ private static ServiceProvider ConfigureServices()
3947
.MinimumLevel.Debug()
4048
.WriteTo.Debug()
4149
.WriteTo.File(
42-
path: "error.log",
50+
path: Path.Combine(LogsDirectory, ".log"),
4351
formatter: new ExpressionTemplate(
4452
"{@t:yyyy-MM-dd HH:mm:ss.fff zzz} [{@l:u3}] {#if SourceContext is not null}[{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}] {#end}{@m}\n{@x}"),
4553
levelSwitch: levelSwitch,
@@ -60,14 +68,20 @@ private static ServiceProvider ConfigureServices()
6068
[STAThread]
6169
public static void Main()
6270
{
63-
Mutex singleInstance = new(true, "f09f929b-e98f-a1e9-9fb3-e383aae383b3" /* This is my favorite GUID */, out bool createdNew);
71+
singleInstanceMutex = new(true, "f09f929b-e98f-a1e9-9fb3-e383aae383b3" /* This is my favorite GUID */, out bool createdNew);
6472
if (!createdNew)
6573
{
6674
return;
6775
}
6876

77+
Environment.CurrentDirectory = AppContext.BaseDirectory;
78+
6979
//CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = new("ja-JP");
7080

81+
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
82+
Application.ThreadException += (sender, e) => HandleCrash(e.Exception);
83+
AppDomain.CurrentDomain.UnhandledException += (sender, e) => HandleCrash((Exception)e.ExceptionObject);
84+
7185
ApplicationConfiguration.Initialize();
7286

7387
// Automatic dark mode is restricted to Win11+ for no good reason. It uses the "AppsUseLightTheme" setting
@@ -79,18 +93,10 @@ public static void Main()
7993
Application.SetColorMode(IsSystemDarkModeEnabled() ? SystemColorMode.Dark : SystemColorMode.Classic);
8094
};
8195

82-
Environment.CurrentDirectory = AppContext.BaseDirectory;
83-
ServiceProvider provider = ConfigureServices();
84-
85-
var logger = provider.GetRequiredService<ILogger>();
96+
provider = ConfigureServices();
97+
logger = provider.GetRequiredService<ILogger>();
8698
logger.Information("Application is starting.");
8799

88-
AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
89-
{
90-
logger.Fatal((Exception)e.ExceptionObject, "Unhandled exception.");
91-
provider.Dispose();
92-
};
93-
94100
Application.ApplicationExit += (_, _) =>
95101
{
96102
logger.Information("Application is exiting.");
@@ -204,4 +210,56 @@ public static bool IsSystemDarkModeEnabled()
204210

205211
return systemUsesLightTheme == 0;
206212
}
213+
214+
private static void HandleCrash(Exception ex)
215+
{
216+
try
217+
{
218+
logger?.Fatal(ex, "Unhandled exception.");
219+
provider?.Dispose();
220+
}
221+
catch { }
222+
223+
try
224+
{
225+
TaskDialogCommandLinkButton restartButton = new(Resources.Restart);
226+
TaskDialogCommandLinkButton exitButton = new(Resources.Exit);
227+
TaskDialogCommandLinkButton logsButton = new(Resources.OpenLogDirectory, allowCloseDialog: false);
228+
229+
string str = ex.ToString();
230+
var stackTraceIndex = str.IndexOf(" at ", StringComparison.OrdinalIgnoreCase);
231+
string text = stackTraceIndex > 0 ? str[..stackTraceIndex].TrimEnd() : str;
232+
TaskDialogExpander? stackTrace = stackTraceIndex > 0 ? new(str[stackTraceIndex..]) : null;
233+
234+
text = text.Replace("\\", "\\\u200B"); // Zero width space to allow paths to wrap instead of getting shortened with an ellipsis
235+
236+
TaskDialogPage taskDialog = new()
237+
{
238+
Heading = Resources.UnhandledException,
239+
Text = text,
240+
Expander = stackTrace,
241+
SizeToContent = true,
242+
Caption = Resources.ProgramName,
243+
Icon = TaskDialogIcon.Error,
244+
Buttons = logger is null ? // Don't show logs button if program crashed during init before logger setup
245+
[restartButton, exitButton] :
246+
[restartButton, exitButton, logsButton],
247+
};
248+
249+
logsButton.Click += (_, _) =>
250+
{
251+
Process.Start(new ProcessStartInfo(LogsDirectory) { UseShellExecute = true });
252+
};
253+
254+
if (TaskDialog.ShowDialog(taskDialog) == restartButton)
255+
{
256+
singleInstanceMutex?.Dispose();
257+
Application.Restart();
258+
return;
259+
}
260+
}
261+
catch { }
262+
263+
Application.Exit();
264+
}
207265
}

AutoAudioSwitcher/Properties/Resources.Designer.cs

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AutoAudioSwitcher/Properties/Resources.ja.resx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<root>
33
<!--
44
Microsoft ResX Schema
@@ -142,4 +142,13 @@
142142
<data name="ProgramName" xml:space="preserve">
143143
<value>オートオーディオスイッチャー</value>
144144
</data>
145+
<data name="UnhandledException" xml:space="preserve">
146+
<value>未処理の例外</value>
147+
</data>
148+
<data name="Restart" xml:space="preserve">
149+
<value>再起動</value>
150+
</data>
151+
<data name="OpenLogDirectory" xml:space="preserve">
152+
<value>ログのディレクトリを開く</value>
153+
</data>
145154
</root>

AutoAudioSwitcher/Properties/Resources.resx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<root>
33
<!--
44
Microsoft ResX Schema
@@ -142,4 +142,13 @@
142142
<data name="ProgramName" xml:space="preserve">
143143
<value>Auto Audio Switcher</value>
144144
</data>
145+
<data name="UnhandledException" xml:space="preserve">
146+
<value>Unhandled exception.</value>
147+
</data>
148+
<data name="Restart" xml:space="preserve">
149+
<value>Restart</value>
150+
</data>
151+
<data name="OpenLogDirectory" xml:space="preserve">
152+
<value>Open log directory</value>
153+
</data>
145154
</root>

0 commit comments

Comments
 (0)