Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DesktopClock/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public partial class App : Application

protected override void OnStartup(StartupEventArgs e)
{
CrashHandler.Register(this);
base.OnStartup(e);
ThemeManager.Initialize();

Expand Down
17 changes: 17 additions & 0 deletions DesktopClock/Properties/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ private Settings()
/// </remarks>
public static bool CanBeSaved { get; private set; }

/// <summary>
/// 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.
/// </summary>
public static void TrySaveIfLoaded()
{
if (!_default.IsValueCreated || !CanBeSaved)
return;

try
{
Default.Save();
}
catch
{
}
}

/// <summary>
/// Checks if the settings file exists on the disk.
/// </summary>
Expand Down
93 changes: 93 additions & 0 deletions DesktopClock/Utilities/CrashHandler.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public static class CrashHandler
{
/// <summary>
/// The log starts over once it grows past this, so repeated crashes can't fill the disk.
/// </summary>
private const long MaxLogSize = 1024 * 1024;

private static int _handling;

/// <summary>
/// Handles unhandled exceptions on the UI thread and on background threads, such as the clock's timer.
/// </summary>
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);
}

/// <summary>
/// Appends the error to a log next to the app, or in the temp folder if that one can't be written to.
/// </summary>
/// <returns>The path of the log that was written, or <c>null</c> if neither could be.</returns>
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
{
}
}
}
Loading