Public Access
86 lines
3.2 KiB
C#
86 lines
3.2 KiB
C#
using DodoSSH.Client.Session;
|
|
|
|
namespace DodoSSH.Client.App.Platform;
|
|
|
|
/// <summary>
|
|
/// Writes down why the application died, in the profile, before it does.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// An exception nothing caught used to close the window and leave nothing behind — no file, no event, no
|
|
/// sentence — so the only report anybody could make was "it closed". This does not keep the process alive:
|
|
/// an exception that reached the top is one no code was written to recover from, and carrying on in whatever
|
|
/// state it left would be a guess. What it adds is the record.
|
|
/// </para>
|
|
/// <para>
|
|
/// Bounded, and appended to rather than rewritten, so a run of crashes keeps its history without the file
|
|
/// growing for ever. Every write is best-effort: a profile that cannot be written to is a reason to lose the
|
|
/// log, never a reason to fail harder.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal static class CrashLog
|
|
{
|
|
/// <summary>Past this, the next entry starts a fresh file.</summary>
|
|
private const long MaxBytes = 1024 * 1024;
|
|
|
|
private static readonly Lock Gate = new();
|
|
|
|
private static string? file;
|
|
|
|
/// <summary>Starts recording, for the process's two last-chance hooks.</summary>
|
|
/// <remarks>
|
|
/// The UI thread's own hook is added by the application once Avalonia is running — see
|
|
/// <see cref="RecordFromTheUiThread"/> — because the dispatcher must not be touched before the platform
|
|
/// has set it up.
|
|
/// </remarks>
|
|
internal static void Install(ClientPaths paths)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(paths);
|
|
|
|
file = paths.CrashLogFile;
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += (_, e) => Write("Unhandled", e.ExceptionObject as Exception);
|
|
|
|
// A fire-and-forget task that faulted. It does not end the process, which is exactly why it needs
|
|
// writing down: the work it was doing simply never happened, and nothing else would ever say so.
|
|
TaskScheduler.UnobservedTaskException += (_, e) =>
|
|
{
|
|
Write("Unobserved task", e.Exception);
|
|
e.SetObserved();
|
|
};
|
|
}
|
|
|
|
/// <summary>The UI thread's hook: records the exception and lets it take the process as before.</summary>
|
|
internal static void RecordFromTheUiThread(Exception exception) => Write("UI thread", exception);
|
|
|
|
/// <summary>Appends one entry; never throws.</summary>
|
|
internal static void Write(string where, Exception? exception)
|
|
{
|
|
if (file is not { } path)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var entry = $"{TimeProvider.System.GetUtcNow():O} {where}{Environment.NewLine}{exception}{Environment.NewLine}{Environment.NewLine}";
|
|
|
|
lock (Gate)
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
|
|
if (File.Exists(path) && new FileInfo(path).Length > MaxBytes)
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
|
|
File.AppendAllText(path, entry);
|
|
}
|
|
catch (Exception failure) when (failure is IOException or UnauthorizedAccessException)
|
|
{
|
|
// Best-effort, by design: see the type's remarks.
|
|
}
|
|
}
|
|
}
|
|
}
|