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
}
}
}