diff --git a/DesktopClock/App.xaml.cs b/DesktopClock/App.xaml.cs index a24f451..13b6ceb 100644 --- a/DesktopClock/App.xaml.cs +++ b/DesktopClock/App.xaml.cs @@ -18,6 +18,7 @@ public partial class App : Application protected override void OnStartup(StartupEventArgs e) { + CrashHandler.Register(this); base.OnStartup(e); ThemeManager.Initialize(); diff --git a/DesktopClock/Properties/Settings.cs b/DesktopClock/Properties/Settings.cs index a6576bc..a24b6e5 100644 --- a/DesktopClock/Properties/Settings.cs +++ b/DesktopClock/Properties/Settings.cs @@ -85,6 +85,23 @@ private Settings() /// public static bool CanBeSaved { get; private set; } + /// + /// Saves one last time if the settings were loaded and can be saved, without throwing, for when the app is about to close after a crash. + /// + public static void TrySaveIfLoaded() + { + if (!_default.IsValueCreated || !CanBeSaved) + return; + + try + { + Default.Save(); + } + catch + { + } + } + /// /// Checks if the settings file exists on the disk. /// diff --git a/DesktopClock/Utilities/CrashHandler.cs b/DesktopClock/Utilities/CrashHandler.cs new file mode 100644 index 0000000..357a618 --- /dev/null +++ b/DesktopClock/Utilities/CrashHandler.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows; +using DesktopClock.Properties; + +namespace DesktopClock.Utilities; + +/// +/// Handles crashes so the clock doesn't just vanish: it saves settings one last time, writes the error to a log file, tells the user where it is, and then exits. +/// +public static class CrashHandler +{ + /// + /// The log starts over once it grows past this, so repeated crashes can't fill the disk. + /// + private const long MaxLogSize = 1024 * 1024; + + private static int _handling; + + /// + /// Handles unhandled exceptions on the UI thread and on background threads, such as the clock's timer. + /// + public static void Register(Application app) + { + app.DispatcherUnhandledException += (_, e) => HandleAndExit(e.Exception); + AppDomain.CurrentDomain.UnhandledException += (_, e) => HandleAndExit(e.ExceptionObject as Exception); + } + + private static void HandleAndExit(Exception exception) + { + // Only the first crash is handled; another one while it's being handled exits right away instead of stacking up dialogs. + if (Interlocked.Exchange(ref _handling, 1) == 0) + { + Settings.TrySaveIfLoaded(); + var logPath = TryWriteLog(exception); + ShowMessage(logPath); + } + + // Staying open in an unknown state could leave a clock that looks fine but has stopped, so always close. + Environment.Exit(1); + } + + /// + /// Appends the error to a log next to the app, or in the temp folder if that one can't be written to. + /// + /// The path of the log that was written, or null if neither could be. + private static string TryWriteLog(Exception exception) + { + var fileName = Path.GetFileNameWithoutExtension(App.MainFileInfo.Name) + ".log"; + var entry = + $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz}\r\n" + + $"{App.MainFileDisplayText} on {RuntimeInformation.OSDescription.Trim()}, {RuntimeInformation.FrameworkDescription}\r\n" + + $"{exception}\r\n\r\n"; + + foreach (var folder in new[] { App.MainFileInfo.DirectoryName, Path.GetTempPath() }) + { + try + { + var path = Path.Combine(folder, fileName); + + if (File.Exists(path) && new FileInfo(path).Length > MaxLogSize) + File.Delete(path); + + File.AppendAllText(path, entry); + return path; + } + catch + { + } + } + + return null; + } + + private static void ShowMessage(string logPath) + { + try + { + var details = logPath == null ? "" : $"Details were saved to:\n{logPath}\n\n"; + + MessageBox.Show( + "DesktopClock ran into a problem and needs to close.\n\n" + + details + + "Please report it at https://github.com/danielchalmers/DesktopClock/issues and include that file.", + "DesktopClock", MessageBoxButton.OK, MessageBoxImage.Error); + } + catch + { + } + } +}