The creator (2026-10-09): 'we need some sort of log-rotation so that we don't inadvertently fill up a user's drive.' startup.log was uncapped - 23MB and growing every few seconds, because FfmpegEncoder logs every non-progress ffmpeg stderr line. The policy lives in a new Helpers/LogRotation.cs as a pure function over a path (RollIfNeeded(logPath, maxBytes, retainedFiles)), so AppLog's 156 call sites keep their surface and the rule is testable on its own. At 5MB the live file rolls to startup.1.log and one rolled copy is kept - the FILE SET is bounded, not just the live file, so a long stream cannot grow the log without limit. A failed rotation degrades to the old behaviour instead of taking the app down. Pattern reference (cited): Serilog's file sink pairs fileSizeLimitBytes (the cap) with retainedFileCountLimit (how many rolled files to keep) - https://github.com/serilog/serilog-sinks-file Guard: ytLive.Tests/AppLogRotationTests.cs - the ONE test drives the real AppLog.Write against a temp dir (two internal seams: PathOverride, MaxBytes) and asserts the roll, the fresh live file, and that after a SECOND roll the directory still holds exactly startup.log + startup.1.log. Also fixed a stale catalog row: TASK 12 (master limiter) shipped 2026-08-15 and was still marked 'Queued'.
55 lines
1.8 KiB
C#
55 lines
1.8 KiB
C#
using System.IO;
|
|
|
|
namespace ytLive.Helpers;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static class AppLog
|
|
{
|
|
/// <summary>The cap for the live log file. Past this, it rolls to <c>startup.1.log</c>.</summary>
|
|
private const long DefaultMaxBytes = 5 * 1024 * 1024;
|
|
|
|
/// <summary>How many rolled copies to keep (startup.1.log, startup.2.log, …).</summary>
|
|
private const int RetainedFiles = 1;
|
|
|
|
private static readonly string DefaultLogPath = Path.Combine(
|
|
InstanceProfile.DataRoot, "startup.log");
|
|
|
|
private static readonly object Gate = new();
|
|
|
|
/// <summary>Test seam: point the log somewhere else. Null = the real profile's log.</summary>
|
|
internal static string? PathOverride { get; set; }
|
|
|
|
/// <summary>Test seam: the cap that triggers a roll.</summary>
|
|
internal static long MaxBytes { get; set; } = DefaultMaxBytes;
|
|
|
|
private static string LogPath => PathOverride ?? DefaultLogPath;
|
|
|
|
public static void Write(string message)
|
|
{
|
|
try
|
|
{
|
|
lock (Gate)
|
|
{
|
|
var path = LogPath;
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
LogRotation.RollIfNeeded(path, MaxBytes, RetainedFiles);
|
|
File.AppendAllText(path, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Never let logging itself crash the app.
|
|
}
|
|
}
|
|
|
|
public static void Write(Exception exception, string message)
|
|
{
|
|
Write($"{message}: {exception}");
|
|
}
|
|
}
|