using System; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace LibationFileManager; public enum StartupLogLevel { Debug, Information, Warning, Error, } public sealed record StartupLogEntry(DateTimeOffset Timestamp, StartupLogLevel Level, string Message, Exception? Exception); /// /// Logging for the startup window that runs before , where /// Serilog.Log.Logger is still Serilog's silent logger and writes nowhere. /// /// Nothing here names a Serilog type, which is the point. Libation's releases are ReadyToRun, so a /// Serilog reference resolves lazily when the line holding it runs; if the install folder's /// Serilog.dll is missing or older than the one this build was compiled against, that line throws. /// A catch block cannot protect its own logging call, so an install broken badly enough to need /// the upgrade recovery in was the exact case in which that recovery /// destroyed the real exception and aborted. See issue #2001. /// /// Entries recorded before are buffered and handed over in order once real logging /// exists, so startup diagnostics reach the log file instead of vanishing. Every method here swallows its /// own failures: this must never be the reason startup ends. /// public static class StartupLog { /// /// Room for a whole startup and then some. Bounded so a caller in a retry loop cannot grow this /// without limit while there is still no sink to drain it. /// private const int MaxBufferedEntries = 500; private static readonly object Gate = new(); private static readonly List Buffered = []; private static Action? Sink; public static void Debug(string message) => Record(StartupLogLevel.Debug, message, null); public static void Debug(Exception? exception, string message) => Record(StartupLogLevel.Debug, message, exception); public static void Information(string message) => Record(StartupLogLevel.Information, message, null); public static void Warning(string message) => Record(StartupLogLevel.Warning, message, null); public static void Warning(Exception? exception, string message) => Record(StartupLogLevel.Warning, message, exception); public static void Error(string message) => Record(StartupLogLevel.Error, message, null); public static void Error(Exception? exception, string message) => Record(StartupLogLevel.Error, message, exception); /// /// Hands every buffered entry to in the order it was recorded, then sends /// later entries straight through. Call once, immediately after logging is configured. /// public static void ReplayTo(Action sink) { ArgumentNullException.ThrowIfNull(sink); StartupLogEntry[] pending; lock (Gate) { Sink = sink; pending = [.. Buffered]; Buffered.Clear(); } foreach (var entry in pending) Deliver(sink, entry); } /// Entries recorded but not yet handed to a sink. public static IReadOnlyList BufferedEntries { get { lock (Gate) return [.. Buffered]; } } /// Drops the sink and anything buffered. Test support: this is process-wide state. public static void ResetForTests() { lock (Gate) { Sink = null; Buffered.Clear(); } } private static void Record(StartupLogLevel level, string message, Exception? exception) { try { var entry = new StartupLogEntry(DateTimeOffset.Now, level, message, exception); Action? sink; lock (Gate) { sink = Sink; if (sink is null) { if (Buffered.Count < MaxBufferedEntries) Buffered.Add(entry); return; } } Deliver(sink, entry); } catch { // A logger that can end startup is worse than no logger at all. } } /// /// The sink is where Serilog lives, so it gets its own frame that is never inlined into a caller. /// A load failure raised inside it is then contained here rather than surfacing in the middle of /// whatever startup step asked for the log line. /// [MethodImpl(MethodImplOptions.NoInlining)] private static void Deliver(Action sink, StartupLogEntry entry) { try { sink(entry); } catch { // see Record } } }