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.
This commit is contained in:
2026-05-07 01:59:28 +10:00
parent 350df71b4f
commit 36fba947e1
2 changed files with 17 additions and 10 deletions
@@ -163,32 +163,39 @@ public class StatusController : ControllerBase
/// <summary>Returns the full log file from the path in effective config (log-facility). Untruncated.</summary> /// <summary>Returns the full log file from the path in effective config (log-facility). Untruncated.</summary>
[HttpGet("logs/download")] [HttpGet("logs/download")]
public async Task<IActionResult> GetLogsDownload(CancellationToken ct) public Task<IActionResult> GetLogsDownload(CancellationToken ct)
{ {
var (effectiveConfig, _) = _configSetService.GetEffectiveConfigWithSources(); var (effectiveConfig, _) = _configSetService.GetEffectiveConfigWithSources();
var logsPath = EffectiveDnsmasqConfig.GetLogsPath(effectiveConfig); var logsPath = EffectiveDnsmasqConfig.GetLogsPath(effectiveConfig);
if (string.IsNullOrEmpty(logsPath)) if (string.IsNullOrEmpty(logsPath))
return NotFound(); return Task.FromResult<IActionResult>(NotFound());
try try
{ {
ct.ThrowIfCancellationRequested();
_logger.LogDebug("Get status logs download"); _logger.LogDebug("Get status logs download");
var fullPath = Path.IsPathRooted(logsPath) ? Path.GetFullPath(logsPath) : Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), logsPath)); var fullPath = Path.IsPathRooted(logsPath) ? Path.GetFullPath(logsPath) : Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), logsPath));
if (!System.IO.File.Exists(fullPath)) if (!System.IO.File.Exists(fullPath))
return NotFound(); return Task.FromResult<IActionResult>(NotFound());
var content = await System.IO.File.ReadAllTextAsync(fullPath, System.Text.Encoding.UTF8, ct);
var fileName = Path.GetFileName(fullPath); var fileName = Path.GetFileName(fullPath);
if (string.IsNullOrEmpty(fileName)) fileName = "dnsmasq.log"; if (string.IsNullOrEmpty(fileName)) fileName = "dnsmasq.log";
return File(
System.Text.Encoding.UTF8.GetBytes(content), // ReadAllTextAsync pre-allocates from Length; concurrent log writes can grow the file mid-read
"text/plain", // and trigger ArgumentOutOfRangeException. Stream with FileShare.ReadWrite and stream the response.
fileName); var stream = new FileStream(
fullPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete,
bufferSize: 64 * 1024,
FileOptions.Asynchronous | FileOptions.SequentialScan);
return Task.FromResult<IActionResult>(File(stream, "text/plain; charset=utf-8", fileName));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Status logs download failed"); _logger.LogError(ex, "Status logs download failed");
return StatusCode(500, new { error = ex.Message }); return Task.FromResult<IActionResult>(StatusCode(500, new { error = ex.Message }));
} }
} }
} }
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.9</Version> <Version>0.0.10</Version>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<!-- Avoid pre-compressed .br/.gz so MapStaticAssets serves uncompressed files (fixes 0-byte/empty response bug). --> <!-- Avoid pre-compressed .br/.gz so MapStaticAssets serves uncompressed files (fixes 0-byte/empty response bug). -->
<DisableBuildCompression>true</DisableBuildCompression> <DisableBuildCompression>true</DisableBuildCompression>