From bec07ae00d782bf3bb3c92fccd036cb8550d0358 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Tue, 22 Sep 2026 14:38:21 -0500 Subject: [PATCH 1/2] Log crashes and tell the user instead of vanishing An unhandled exception closed the clock with no message, leaving only an entry in Event Viewer. Now the first one, on the UI thread or a background thread like the clock's timer, is appended to DesktopClock.log next to the app (or in the temp folder if that one can't be written to) with the time, version, Windows and .NET versions, and the stack trace. A message says where the log is and where to report it, then the app exits, since carrying on in an unknown state could leave a clock that looks fine but has stopped. A second crash while that's happening exits right away instead of stacking dialogs, and the log starts over past 1 MB. --- DesktopClock/App.xaml.cs | 1 + DesktopClock/Utilities/CrashHandler.cs | 91 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 DesktopClock/Utilities/CrashHandler.cs 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/Utilities/CrashHandler.cs b/DesktopClock/Utilities/CrashHandler.cs new file mode 100644 index 0000000..1ac7142 --- /dev/null +++ b/DesktopClock/Utilities/CrashHandler.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows; + +namespace DesktopClock.Utilities; + +/// +/// Handles crashes so the clock doesn't just vanish: it 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) + { + 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 + { + } + } +} From 00309a4d9e5fd8e40996d210619a56f1d2ab7ebc Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Tue, 22 Sep 2026 14:38:40 -0500 Subject: [PATCH 2/2] Save settings one last time before closing after a crash Settings are saved a second after they change, so a crash within that second lost the last change. The crash handler now saves them first if they were loaded and can be saved, ignoring any error so it never gets in the way of logging. A setting changed 0.3 seconds before a crash is kept now. --- DesktopClock/Properties/Settings.cs | 17 +++++++++++++++++ DesktopClock/Utilities/CrashHandler.cs | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) 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 index 1ac7142..357a618 100644 --- a/DesktopClock/Utilities/CrashHandler.cs +++ b/DesktopClock/Utilities/CrashHandler.cs @@ -3,11 +3,12 @@ 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 writes the error to a log file, tells the user where it is, and then exits. +/// 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 { @@ -32,6 +33,7 @@ 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); }