diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a2f4d7..7a981ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.57.8] - 2026-08-06
+
+### Fixed
+- **The log file can no longer grow big enough to be unusable.** SysManager keeps a diary of what it does, and that file is what you attach when reporting a problem. It had no size limit — one day's file was allowed to reach a gigabyte, which is far past what you could upload anywhere. Each file is now capped at 10 MB and a fresh one is started when it fills, keeping at most two weeks of them, so the diary stays small enough to send and takes a predictable amount of disk.
+- **The Volume Control tab no longer floods that log.** If an app's audio could not be read, the tab wrote the same complaint twenty times a second for as long as it stayed open — filling the file with one repeated line and pushing out everything actually worth reading. It is now noted once, and again only if something genuinely changes.
+
## [1.57.7] - 2026-08-06
### Fixed
diff --git a/SysManager/SysManager.Tests/LogServiceSinkScrubbingTests.cs b/SysManager/SysManager.Tests/LogServiceSinkScrubbingTests.cs
index f620732..ab6ee3d 100644
--- a/SysManager/SysManager.Tests/LogServiceSinkScrubbingTests.cs
+++ b/SysManager/SysManager.Tests/LogServiceSinkScrubbingTests.cs
@@ -196,4 +196,61 @@ public void Sink_IsSafeUnderConcurrentWrites()
// Every line is individually intact — no two events rendered into each other.
Assert.All(lines, line => Assert.Matches(@"\[INF\] Line \d+ at ", line));
}
+
+ // ── The log file is bounded ──────────────────────────────────────────────
+ // The sink passed no fileSizeLimitBytes and no rollOnFileSizeLimit, so it took Serilog's
+ // defaults: 1 GB per file with rolling OFF. The only bound on the folder was the 14-FILE count,
+ // so one daily file could grow to a gigabyte. Debug is a real volume tier here (290 Log.Debug
+ // call sites) and the documented support path is "attach the log" — an unattachable file breaks
+ // the evidence trail exactly when it is needed.
+
+ [Fact]
+ public void TheSizeLimit_IsSmallEnoughToAttachToABugReport()
+ {
+ // 25 MB is GitHub's per-file attachment ceiling. A log the user cannot upload is the failure
+ // this bound exists to prevent, so the intent is pinned rather than just the number.
+ Assert.True(LogService.MaxLogFileBytes <= 25L * 1024 * 1024,
+ $"A {LogService.MaxLogFileBytes / 1024 / 1024} MB log file is too large to attach to an issue.");
+ Assert.True(LogService.MaxLogFileBytes >= 1024 * 1024,
+ "Too small to hold useful context for a crash report.");
+ }
+
+ [Fact]
+ public void TheWholeLogFolder_IsBounded()
+ {
+ // The point of the pair: per-file ceiling × retained count is a predictable worst case,
+ // which is what the 14-file-count-alone version never gave.
+ var worstCase = LogService.MaxLogFileBytes * LogService.RetainedFileCount;
+ Assert.True(worstCase <= 250L * 1024 * 1024,
+ $"Worst-case log folder is {worstCase / 1024 / 1024} MB — too much for a low-end laptop.");
+ }
+
+ [Fact]
+ public void TheSink_KeepsEveryFileUnderTheLimit()
+ {
+ // Asserting the constants alone would pass even if the sink never received them — the defect
+ // was a missing ARGUMENT, not a wrong number. So drive the real sink hard and check the files
+ // it actually produced.
+ //
+ // RollingInterval.Infinite so the only thing that could create a second file is the size
+ // limit: with a daily interval a date change could produce one and this would pass for the
+ // wrong reason.
+ var sinkType = typeof(LogService).GetNestedType("UserPathScrubbingSink", BindingFlags.NonPublic)!;
+ var path = Path.Combine(_dir, "roll.log");
+ var sink = (ILogEventSink)Activator.CreateInstance(
+ sinkType, [path, RollingInterval.Infinite, LogService.RetainedFileCount])!;
+
+ using (var logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Sink(sink).CreateLogger())
+ {
+ var filler = new string('x', 1024); // ~1 KB per line
+ for (int i = 0; i < 2000; i++) logger.Information("{Index} {Filler}", i, filler);
+ }
+ (sink as IDisposable)?.Dispose();
+
+ var written = Directory.GetFiles(_dir, "roll*.log");
+ Assert.NotEmpty(written);
+ Assert.All(written, f =>
+ Assert.True(new FileInfo(f).Length <= LogService.MaxLogFileBytes + 64 * 1024,
+ $"{Path.GetFileName(f)} is {new FileInfo(f).Length} bytes, over the {LogService.MaxLogFileBytes}-byte limit."));
+ }
}
diff --git a/SysManager/SysManager/Services/AudioMixerService.cs b/SysManager/SysManager/Services/AudioMixerService.cs
index 2a6073e..628f37c 100644
--- a/SysManager/SysManager/Services/AudioMixerService.cs
+++ b/SysManager/SysManager/Services/AudioMixerService.cs
@@ -287,12 +287,29 @@ public float GetPeak(string sessionId)
{
if (meter.GetPeakValue(out float value) == 0 && value > peak) peak = value;
}
- catch (COMException ex) { Log.Debug("GetPeak failed: {Error}", ex.Message); }
+ catch (COMException ex)
+ {
+ // Log once per session-handle generation, not per call. GetPeak is driven by the
+ // Volume Control tab's 50 ms peak-meter timer, so one bad audio session wrote
+ // ~20 identical Debug lines a second for as long as the tab stayed open — the
+ // log's single biggest flood path. The message is identical every tick, so
+ // repeating it adds nothing; ReleaseGroups resets the flag, meaning a genuinely
+ // new failure after a device change is still reported. Mirrors the existing
+ // one-shot probe pattern (_routingProbed / _routingSupported).
+ if (!_peakFailureLogged)
+ {
+ _peakFailureLogged = true;
+ Log.Debug("GetPeak failed: {Error}", ex.Message);
+ }
+ }
}
return peak;
}
}
+ // Reset by ReleaseGroups, so it is per session-handle generation rather than per process.
+ private bool _peakFailureLogged;
+
// ── Output-device enumeration (documented API) ─────────────────────────
///
@@ -492,6 +509,9 @@ private void ReleaseGroups()
foreach (var control in controls)
Release(control);
_groups.Clear();
+ // New handles mean a new chance to succeed — and a failure against them is new information,
+ // so allow it to be logged once more rather than staying silent for the whole session.
+ _peakFailureLogged = false;
}
private static void Release(object? comObject)
diff --git a/SysManager/SysManager/Services/LogService.cs b/SysManager/SysManager/Services/LogService.cs
index 063d51b..439cc8b 100644
--- a/SysManager/SysManager/Services/LogService.cs
+++ b/SysManager/SysManager/Services/LogService.cs
@@ -17,6 +17,16 @@ public static partial class LogService
{
public static Logger? Logger { get; private set; }
+ ///
+ /// Per-file ceiling for the rolling log. With this bounds the
+ /// whole folder at roughly 140 MB, and keeps any single file small enough to attach to a bug
+ /// report — which is the documented way a user sends us evidence.
+ ///
+ internal const long MaxLogFileBytes = 10L * 1024 * 1024;
+
+ /// How many rolled files to keep. Combines with .
+ internal const int RetainedFileCount = 14;
+
public static string LogDir { get; } =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SysManager", "logs");
@@ -59,7 +69,7 @@ public static void Init()
.WriteTo.Sink(new UserPathScrubbingSink(
Path.Combine(LogDir, "sysmanager-.log"),
rollingInterval: RollingInterval.Day,
- retainedFileCountLimit: 14))
+ retainedFileCountLimit: RetainedFileCount))
.CreateLogger();
Log.Logger = Logger;
Logger.Information("SysManager started");
@@ -98,6 +108,15 @@ public UserPathScrubbingSink(
path,
rollingInterval: rollingInterval,
retainedFileCountLimit: retainedFileCountLimit,
+ // Bound each file AND roll when it fills. Without both, the sink took Serilog's
+ // defaults — 1 GB per file with rollOnFileSizeLimit false — so a single daily
+ // file could grow to a gigabyte with nothing rolling below it, and the only
+ // bound on the folder was the 14-FILE count. That matters because the whole
+ // support path is "attach the log" (SUPPORT.md, the bug-report template): a file
+ // too large to upload breaks the evidence trail exactly when it is needed, and
+ // Debug is a real volume tier here (290 Log.Debug call sites, some in loops).
+ fileSizeLimitBytes: MaxLogFileBytes,
+ rollOnFileSizeLimit: true,
outputTemplate: "{Message:lj}{NewLine}")
.CreateLogger();
}
diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj
index bcb4544..808f8f2 100644
--- a/SysManager/SysManager/SysManager.csproj
+++ b/SysManager/SysManager/SysManager.csproj
@@ -10,9 +10,9 @@
SysManager
true
NU1603;NU1701
- 1.57.7
- 1.57.7.0
- 1.57.7.0
+ 1.57.8
+ 1.57.8.0
+ 1.57.8.0
SysManager
SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.
https://github.com/laurentiu021/SystemManager