using System.IO; namespace ytLive.Helpers; /// Presentation category; independent from the entry's attention class. public enum AppLogMessageType { Information, Success, Warning, Error, } /// Explicit attention priority, assigned by the writer rather than inferred from text or source. public enum AppLogErrorClass { /// Fatal or unhandled failure; highest priority. Class1 = 1, /// Recoverable failure that still requires attention. Class2 = 2, } /// /// Minimal file logger used to diagnose startup/shutdown crashes on Windows, /// where the WPF app has no visible console. Writes to /// %APPDATA%\ytLlive\startup.log (or a per-instance subfolder in DEBUG — see /// InstanceProfile), appending synchronously so entries survive a hard crash. /// public static class AppLog { /// The cap for the live log file. Past this, it rolls to startup.1.log. private const long DefaultMaxBytes = 5 * 1024 * 1024; private const int CurrentLaunchEntryLimit = 500; /// How many rolled copies to keep (startup.1.log, startup.2.log, …). private const int RetainedFiles = 1; private static readonly string DefaultLogPath = Path.Combine( InstanceProfile.DataRoot, "startup.log"); private static readonly object Gate = new(); private static readonly List CurrentLaunchEntries = new(); private static long _nextSequence; /// Test seam: point the log somewhere else. Null = the real profile's log. internal static string? PathOverride { get; set; } /// Test seam: the cap that triggers a roll. internal static long MaxBytes { get; set; } = DefaultMaxBytes; private static string LogPath => PathOverride ?? DefaultLogPath; internal static IReadOnlyList SnapshotCurrentLaunch() { lock (Gate) return CurrentLaunchEntries.ToArray(); } public static void Write(string message) => Write(message, AppLogMessageType.Information); public static void WriteWarning(string message) => Write(message, AppLogMessageType.Warning); public static void WriteSuccess(string message) => Write(message, AppLogMessageType.Success); public static void Write(string message, AppLogMessageType type, AppLogErrorClass? errorClass = null) { try { lock (Gate) { var entry = new AppLogEntry( ++_nextSequence, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}", type, errorClass); CurrentLaunchEntries.Add(entry); if (CurrentLaunchEntries.Count > CurrentLaunchEntryLimit) CurrentLaunchEntries.RemoveAt(0); var path = LogPath; Directory.CreateDirectory(Path.GetDirectoryName(path)!); LogRotation.RollIfNeeded(path, MaxBytes, RetainedFiles); File.AppendAllText(path, $"{entry.Text}{Environment.NewLine}"); } } catch { // Never let logging itself crash the app. } } public static void Write(Exception exception, string message) { Write($"{message}: {exception}", AppLogMessageType.Error); } public static void Write(Exception exception, string message, AppLogErrorClass errorClass) => Write($"{message}: {exception}", AppLogMessageType.Error, errorClass); } public sealed record AppLogEntry(long Sequence, string Text, AppLogMessageType Type, AppLogErrorClass? ErrorClass) { public string? ErrorClassLabel => ErrorClass switch { AppLogErrorClass.Class1 => "CLASS 1", AppLogErrorClass.Class2 => "CLASS 2", _ => null, }; public string TypeLabel => ErrorClassLabel ?? Type switch { AppLogMessageType.Information => "INFO", AppLogMessageType.Success => "SUCCESS", AppLogMessageType.Warning => "WARNING", AppLogMessageType.Error => "ERROR", _ => Type.ToString().ToUpperInvariant(), }; }