using DodoSSH.Client.Session; namespace DodoSSH.Client.App.Platform; /// /// Writes down why the application died, in the profile, before it does. /// /// /// /// 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. /// /// /// 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. /// /// internal static class CrashLog { /// Past this, the next entry starts a fresh file. private const long MaxBytes = 1024 * 1024; private static readonly Lock Gate = new(); private static string? file; /// Starts recording, for the process's two last-chance hooks. /// /// The UI thread's own hook is added by the application once Avalonia is running — see /// — because the dispatcher must not be touched before the platform /// has set it up. /// 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(); }; } /// The UI thread's hook: records the exception and lets it take the process as before. internal static void RecordFromTheUiThread(Exception exception) => Write("UI thread", exception); /// Appends one entry; never throws. 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. } } } }