From 36fba947e1bf2a267e3781cfa52ea45e93781076 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Thu, 7 May 2026 01:59:28 +1000 Subject: [PATCH] Fix log download race; stream live log file (v0.0.10) Replace ReadAllTextAsync with FileStream + FileShare.ReadWrite so concurrent dnsmasq writes cannot trigger StringBuilder capacity errors. Release as 0.0.10. --- .../Controllers/StatusController.cs | 25 ++++++++++++------- src/DnsmasqWebUI/DnsmasqWebUI.csproj | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/DnsmasqWebUI/Controllers/StatusController.cs b/src/DnsmasqWebUI/Controllers/StatusController.cs index f95af13..a942cb2 100644 --- a/src/DnsmasqWebUI/Controllers/StatusController.cs +++ b/src/DnsmasqWebUI/Controllers/StatusController.cs @@ -163,32 +163,39 @@ public class StatusController : ControllerBase /// Returns the full log file from the path in effective config (log-facility). Untruncated. [HttpGet("logs/download")] - public async Task GetLogsDownload(CancellationToken ct) + public Task GetLogsDownload(CancellationToken ct) { var (effectiveConfig, _) = _configSetService.GetEffectiveConfigWithSources(); var logsPath = EffectiveDnsmasqConfig.GetLogsPath(effectiveConfig); if (string.IsNullOrEmpty(logsPath)) - return NotFound(); + return Task.FromResult(NotFound()); try { + ct.ThrowIfCancellationRequested(); _logger.LogDebug("Get status logs download"); var fullPath = Path.IsPathRooted(logsPath) ? Path.GetFullPath(logsPath) : Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), logsPath)); if (!System.IO.File.Exists(fullPath)) - return NotFound(); + return Task.FromResult(NotFound()); - var content = await System.IO.File.ReadAllTextAsync(fullPath, System.Text.Encoding.UTF8, ct); var fileName = Path.GetFileName(fullPath); if (string.IsNullOrEmpty(fileName)) fileName = "dnsmasq.log"; - return File( - System.Text.Encoding.UTF8.GetBytes(content), - "text/plain", - fileName); + + // ReadAllTextAsync pre-allocates from Length; concurrent log writes can grow the file mid-read + // and trigger ArgumentOutOfRangeException. Stream with FileShare.ReadWrite and stream the response. + var stream = new FileStream( + fullPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete, + bufferSize: 64 * 1024, + FileOptions.Asynchronous | FileOptions.SequentialScan); + return Task.FromResult(File(stream, "text/plain; charset=utf-8", fileName)); } catch (Exception ex) { _logger.LogError(ex, "Status logs download failed"); - return StatusCode(500, new { error = ex.Message }); + return Task.FromResult(StatusCode(500, new { error = ex.Message })); } } } diff --git a/src/DnsmasqWebUI/DnsmasqWebUI.csproj b/src/DnsmasqWebUI/DnsmasqWebUI.csproj index 8d3485b..0625a2a 100644 --- a/src/DnsmasqWebUI/DnsmasqWebUI.csproj +++ b/src/DnsmasqWebUI/DnsmasqWebUI.csproj @@ -3,7 +3,7 @@ net10.0 enable - 0.0.9 + 0.0.10 enable true