From 66f7dc297fd4a8b3691af7ce4b6db81b0ee7635f Mon Sep 17 00:00:00 2001 From: Jonathan Arndt Date: Sat, 29 Aug 2026 11:29:41 -0700 Subject: [PATCH 1/2] fix(logging): cap rolling debug log file size to prevent disk exhaustion Add configurable fileSizeLimitBytes + rollOnFileSizeLimit to the Serilog file sink so the daily rolling log set cannot fill constrained storage (e.g. RMC4 with \user on a small removable USB drive). Caps persist in CrestronDataStore and are settable via the new applogfilecap console command (effective next restart), with conservative appliance defaults (4 MB x 7 files). Refs PepperDash/Essentials#1472 --- src/PepperDash.Core/Logging/Debug.cs | 103 ++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 97ac5890..ad505d71 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -26,6 +26,27 @@ namespace PepperDash.Core private static readonly string WebSocketLevelStoreKey = "WebsocketDebugLevel"; private static readonly string ErrorLogLevelStoreKey = "ErrorLogDebugLevel"; private static readonly string FileLevelStoreKey = "FileDebugLevel"; + private static readonly string FileSizeLimitStoreKey = "FileLogSizeLimitBytes"; + private static readonly string FileRetainedCountStoreKey = "FileLogRetainedCount"; + + // Conservative defaults so the daily rolling log set cannot exhaust + // constrained storage (e.g. RMC4 with \user on a small removable drive). + private const long DefaultApplianceFileSizeLimitBytes = 4L * 1024 * 1024; + private const long DefaultServerFileSizeLimitBytes = 16L * 1024 * 1024; + private const int DefaultApplianceRetainedFileCount = 7; + private const int DefaultServerRetainedFileCount = 14; + + /// + /// Per-file size (bytes) that triggers a roll of the debug log file. Read at + /// startup from CrestronDataStore; override with the applogfilecap console command. + /// + public static long LogFileSizeLimitBytes { get; private set; } + + /// + /// Number of rolled debug log files retained on disk. Combined with + /// LogFileSizeLimitBytes this bounds total debug-log disk usage. + /// + public static int LogRetainedFileCountLimit { get; private set; } private static readonly Dictionary _logLevels = new Dictionary() { @@ -157,6 +178,12 @@ namespace PepperDash.Core ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + var isAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; + LogFileSizeLimitBytes = GetStoredIntValue(FileSizeLimitStoreKey, + (int)(isAppliance ? DefaultApplianceFileSizeLimitBytes : DefaultServerFileSizeLimitBytes)); + LogRetainedFileCountLimit = GetStoredIntValue(FileRetainedCountStoreKey, + isAppliance ? DefaultApplianceRetainedFileCount : DefaultServerRetainedFileCount); + _defaultLoggerConfiguration = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() @@ -166,8 +193,10 @@ namespace PepperDash.Core .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: _errorLogLevelSwitch) .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: LogFileSizeLimitBytes, + rollOnFileSizeLimit: true, restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 7 : 14, + retainedFileCountLimit: LogRetainedFileCountLimit, levelSwitch: _fileLogLevelSwitch ); @@ -223,6 +252,9 @@ namespace PepperDash.Core ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(SetLogFileCapFromConsole, "applogfilecap", + "applogfilecap:P [sizeBytes] [retainedCount]: Caps rolling debug log size/count (next restart)", + ConsoleAccessLevelEnum.AccessOperator); } CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; @@ -297,6 +329,75 @@ namespace PepperDash.Core } } + private static int GetStoredIntValue(string storeKey, int defaultValue) + { + try + { + var result = CrestronDataStoreStatic.GetLocalIntValue(storeKey, out int value); + + if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + CrestronDataStoreStatic.SetLocalIntValue(storeKey, defaultValue); + return defaultValue; + } + + return value <= 0 ? defaultValue : value; + } + catch (Exception ex) + { + CrestronConsole.PrintLine($"Exception retrieving stored value for {storeKey}: {ex.Message}"); + return defaultValue; + } + } + + /// + /// Console handler to cap the rolling debug log file size and retained count. + /// Values persist in CrestronDataStore and take effect on the next program restart. + /// + public static void SetLogFileCapFromConsole(string command) + { + var trimmed = command?.Trim() ?? string.Empty; + + if (trimmed == "?" || string.IsNullOrEmpty(trimmed)) + { + CrestronConsole.ConsoleCommandResponse( + "Caps the rolling debug log file (takes effect on next program restart):\r\n" + + "Usage: applogfilecap:P [sizeBytes] [retainedCount]\r\n" + + " sizeBytes: per-file size that triggers a roll (min 65536)\r\n" + + " retainedCount: number of rolled files to keep (min 1)\r\n" + + $"Current: sizeBytes = {LogFileSizeLimitBytes}, retainedCount = {LogRetainedFileCountLimit}\r\n" + + $"Approx on-disk ceiling = {LogFileSizeLimitBytes * LogRetainedFileCountLimit} bytes\r\n"); + return; + } + + var tokens = trimmed.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + + if (!long.TryParse(tokens[0], out var sizeBytes) || sizeBytes < 65536) + { + CrestronConsole.ConsoleCommandResponse("Invalid sizeBytes. Must be an integer >= 65536.\r\n"); + return; + } + + sizeBytes = Math.Min(sizeBytes, int.MaxValue); + + var retained = LogRetainedFileCountLimit; + if (tokens.Length > 1 && (!int.TryParse(tokens[1], out retained) || retained < 1)) + { + CrestronConsole.ConsoleCommandResponse("Invalid retainedCount. Must be an integer >= 1.\r\n"); + return; + } + + var sizeErr = CrestronDataStoreStatic.SetLocalIntValue(FileSizeLimitStoreKey, (int)sizeBytes); + var countErr = CrestronDataStoreStatic.SetLocalIntValue(FileRetainedCountStoreKey, retained); + + LogFileSizeLimitBytes = sizeBytes; + LogRetainedFileCountLimit = retained; + + CrestronConsole.ConsoleCommandResponse( + $"File log cap stored: sizeBytes = {sizeBytes}, retainedCount = {retained} (store result: {sizeErr}/{countErr}).\r\n" + + "Takes effect on next program restart.\r\n"); + } + private static void GetVersion() { var assembly = Assembly.GetExecutingAssembly(); From c0c6a9fd65eea4a582b6e447b4b41fc45c1922f0 Mon Sep 17 00:00:00 2001 From: Jonathan Arndt Date: Sat, 29 Aug 2026 11:59:59 -0700 Subject: [PATCH 2/2] fix(logging): clamp stored log caps to minimums and reject extra applogfilecap args Addresses Copilot review on #1473: clamp CDS-loaded fileSizeLimitBytes/retainedCount to the enforced floors (65536 bytes / 1 file) at startup so a stale small stored value cannot cause pathological rolling, and reject >2 console tokens to prevent silent misconfiguration. --- src/PepperDash.Core/Logging/Debug.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index ad505d71..f7c580ad 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -36,6 +36,10 @@ namespace PepperDash.Core private const int DefaultApplianceRetainedFileCount = 7; private const int DefaultServerRetainedFileCount = 14; + // Enforced floors so a stale/corrupt stored value can't cause pathological rolling. + private const int MinFileSizeLimitBytes = 65536; + private const int MinRetainedFileCount = 1; + /// /// Per-file size (bytes) that triggers a roll of the debug log file. Read at /// startup from CrestronDataStore; override with the applogfilecap console command. @@ -179,10 +183,10 @@ namespace PepperDash.Core : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; var isAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; - LogFileSizeLimitBytes = GetStoredIntValue(FileSizeLimitStoreKey, - (int)(isAppliance ? DefaultApplianceFileSizeLimitBytes : DefaultServerFileSizeLimitBytes)); - LogRetainedFileCountLimit = GetStoredIntValue(FileRetainedCountStoreKey, - isAppliance ? DefaultApplianceRetainedFileCount : DefaultServerRetainedFileCount); + LogFileSizeLimitBytes = Math.Max(MinFileSizeLimitBytes, GetStoredIntValue(FileSizeLimitStoreKey, + (int)(isAppliance ? DefaultApplianceFileSizeLimitBytes : DefaultServerFileSizeLimitBytes))); + LogRetainedFileCountLimit = Math.Max(MinRetainedFileCount, GetStoredIntValue(FileRetainedCountStoreKey, + isAppliance ? DefaultApplianceRetainedFileCount : DefaultServerRetainedFileCount)); _defaultLoggerConfiguration = new LoggerConfiguration() .MinimumLevel.Verbose() @@ -372,7 +376,13 @@ namespace PepperDash.Core var tokens = trimmed.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); - if (!long.TryParse(tokens[0], out var sizeBytes) || sizeBytes < 65536) + if (tokens.Length > 2) + { + CrestronConsole.ConsoleCommandResponse("Too many arguments. Usage: applogfilecap [sizeBytes] [retainedCount].\r\n"); + return; + } + + if (!long.TryParse(tokens[0], out var sizeBytes) || sizeBytes < MinFileSizeLimitBytes) { CrestronConsole.ConsoleCommandResponse("Invalid sizeBytes. Must be an integer >= 65536.\r\n"); return; @@ -381,7 +391,7 @@ namespace PepperDash.Core sizeBytes = Math.Min(sizeBytes, int.MaxValue); var retained = LogRetainedFileCountLimit; - if (tokens.Length > 1 && (!int.TryParse(tokens[1], out retained) || retained < 1)) + if (tokens.Length > 1 && (!int.TryParse(tokens[1], out retained) || retained < MinRetainedFileCount)) { CrestronConsole.ConsoleCommandResponse("Invalid retainedCount. Must be an integer >= 1.\r\n"); return;