Updated
EonaCat/ci-workflows: SonarQube / sonar (push) Successful in 22s

This commit is contained in:
EonaCat committed 2026-10-08 21:33:54 +02:00
1 parent 4081b5949d
commit 2909500bb8
14 files changed
+342 -114

No files matched your search

+7 -2
View File
@@ -36,7 +36,7 @@ namespace EonaCat.LogStack.Server
_maxLogDirectorySize = maxLogDirectorySize; _maxLogDirectorySize = maxLogDirectorySize;
} }
protected virtual Task ProcessLogAsync(string logData) protected virtual async Task ProcessLogAsync(string logData)
{ {
string logsRootDirectory = "logs"; string logsRootDirectory = "logs";
@@ -67,7 +67,12 @@ namespace EonaCat.LogStack.Server
// After processing log, check directory size and clean up if needed // After processing log, check directory size and clean up if needed
CleanUpOldLogs(); CleanUpOldLogs();
return File.AppendAllTextAsync(logFilePath, logData + Environment.NewLine); using (var stream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete, 4096, FileOptions.Asynchronous))
using (var writer = new StreamWriter(stream))
{
await writer.WriteLineAsync(logData).ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false);
}
} }
private void CleanUpOldLogs() private void CleanUpOldLogs()
+19
View File
@@ -24,6 +24,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Status", "EonaCat.LogStack.Status\EonaCat.LogStack.Status.csproj", "{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Status", "EonaCat.LogStack.Status\EonaCat.LogStack.Status.csproj", "{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testers", "Testers", "{A930DAA9-4165-776A-E33D-6691CB934F50}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.FileDeletionTests", "Testers\EonaCat.LogStack.FileDeletionTests\EonaCat.LogStack.FileDeletionTests.csproj", "{25A49D02-5463-4662-B064-55293976DF0A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -130,10 +134,25 @@ Global
{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x64.Build.0 = Release|Any CPU {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x64.Build.0 = Release|Any CPU
{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.ActiveCfg = Release|Any CPU {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.ActiveCfg = Release|Any CPU
{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.Build.0 = Release|Any CPU {34C47EBC-BB59-0A5C-9D93-416E4F3D7816}.Release|x86.Build.0 = Release|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x64.ActiveCfg = Debug|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x64.Build.0 = Debug|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x86.ActiveCfg = Debug|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Debug|x86.Build.0 = Debug|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Release|Any CPU.Build.0 = Release|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x64.ActiveCfg = Release|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x64.Build.0 = Release|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x86.ActiveCfg = Release|Any CPU
{25A49D02-5463-4662-B064-55293976DF0A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{25A49D02-5463-4662-B064-55293976DF0A} = {A930DAA9-4165-776A-E33D-6691CB934F50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F} SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F}
EndGlobalSection EndGlobalSection
@@ -134,7 +134,11 @@ namespace EonaCat.LogStack.Features
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}); });
File.WriteAllText(filePath, json); using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
using (var writer = new StreamWriter(stream))
{
writer.Write(json);
}
} }
} }
@@ -329,7 +333,11 @@ namespace EonaCat.LogStack.Features
{ {
WriteIndented = true WriteIndented = true
}); });
File.WriteAllText(filePath, json); using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
using (var writer = new StreamWriter(stream))
{
writer.Write(json);
}
} }
public static LogReplayScenario Load(string filePath) public static LogReplayScenario Load(string filePath)
@@ -37,7 +37,7 @@ namespace EonaCat.LogStack.Flows
/// This creates a hash-chain so any deletion or modification of a past /// This creates a hash-chain so any deletion or modification of a past
/// entry invalidates all subsequent hashes, making tampering detectable. /// entry invalidates all subsequent hashes, making tampering detectable.
/// ///
/// The file is opened with FileShare.Read only (no concurrent writers). /// The file permits external readers and deletion, but no concurrent writers.
/// The flow is synchronous-by-design: audit entries must land on disk /// The flow is synchronous-by-design: audit entries must land on disk
/// before the method returns, so <see cref="BlastAsync"/> blocks until /// before the method returns, so <see cref="BlastAsync"/> blocks until
/// the entry is flushed. /// the entry is flushed.
@@ -53,8 +53,8 @@ namespace EonaCat.LogStack.Flows
private readonly bool _includeProperties; private readonly bool _includeProperties;
private readonly object _writeLock = new object(); private readonly object _writeLock = new object();
private readonly FileStream _stream; private FileStream _stream;
private readonly StreamWriter _writer; private StreamWriter _writer;
private long _sequence; private long _sequence;
private string _previousHash; private string _previousHash;
@@ -117,19 +117,23 @@ namespace EonaCat.LogStack.Flows
string date = DateTime.UtcNow.ToString("yyyyMMdd"); string date = DateTime.UtcNow.ToString("yyyyMMdd");
_filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit"); _filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit");
// Exclusive write access OpenFile();
}
private void OpenFile()
{
_stream = new FileStream( _stream = new FileStream(
_filePath, _filePath,
FileMode.Append, FileMode.Append,
FileAccess.Write, FileAccess.Write,
FileShare.Read, // allow external readers, but no other writers FileShare.Read | FileShare.Delete,
bufferSize: 4096, bufferSize: 4096,
FileOptions.WriteThrough); // WriteThrough = no OS cache, hits disk immediately FileOptions.WriteThrough); // WriteThrough = no OS cache, hits disk immediately
_writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true }; _writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true };
// Derive starting hash from the last line already in the file (for continuity) // Derive starting hash from the last line already in the file (for continuity)
_previousHash = ReadLastHash(directory, filePrefix, date); _previousHash = ReadLastHash(_filePath);
_sequence = CountExistingLines(_filePath); _sequence = CountExistingLines(_filePath);
} }
@@ -153,7 +157,10 @@ namespace EonaCat.LogStack.Flows
string previousHash = new string('0', HashLength); string previousHash = new string('0', HashLength);
long expectedSeq = 1; long expectedSeq = 1;
foreach (string raw in File.ReadLines(filePath, Encoding.UTF8)) using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var reader = new StreamReader(stream, Encoding.UTF8);
string raw;
while ((raw = reader.ReadLine()) != null)
{ {
if (string.IsNullOrWhiteSpace(raw) || raw.StartsWith("#")) if (string.IsNullOrWhiteSpace(raw) || raw.StartsWith("#"))
{ {
@@ -252,6 +259,7 @@ namespace EonaCat.LogStack.Flows
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
{ {
IsEnabled = false; IsEnabled = false;
await base.DisposeAsync().ConfigureAwait(false);
lock (_writeLock) lock (_writeLock)
{ {
try { _writer.Flush(); } catch { } try { _writer.Flush(); } catch { }
@@ -259,13 +267,19 @@ namespace EonaCat.LogStack.Flows
try { _writer.Dispose(); } catch { } try { _writer.Dispose(); } catch { }
try { _stream.Dispose(); } catch { } try { _stream.Dispose(); } catch { }
} }
await base.DisposeAsync().ConfigureAwait(false);
} }
private void WriteEntry(LogEvent log) private void WriteEntry(LogEvent log)
{ {
lock (_writeLock) lock (_writeLock)
{ {
if (!File.Exists(_filePath))
{
_writer.Dispose();
_stream.Dispose();
OpenFile();
}
long seq = Interlocked.Increment(ref _sequence); long seq = Interlocked.Increment(ref _sequence);
var sb = new StringBuilder(256); var sb = new StringBuilder(256);
@@ -356,10 +370,8 @@ namespace EonaCat.LogStack.Flows
} }
} }
private static string ReadLastHash(string directory, string prefix, string date) private static string ReadLastHash(string path)
{ {
string path = Path.Combine(directory, $"{prefix}_{Environment.MachineName}_{date}.audit");
if (!File.Exists(path)) if (!File.Exists(path))
{ {
return new string('0', HashLength); return new string('0', HashLength);
@@ -368,7 +380,7 @@ namespace EonaCat.LogStack.Flows
string lastLine = null; string lastLine = null;
// Open file with FileShare.ReadWrite to allow reading while it's being written to // Open file with FileShare.ReadWrite to allow reading while it's being written to
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
using (var reader = new StreamReader(fileStream, Encoding.UTF8)) using (var reader = new StreamReader(fileStream, Encoding.UTF8))
{ {
// Read lines from the file // Read lines from the file
@@ -402,7 +414,7 @@ namespace EonaCat.LogStack.Flows
long count = 0; long count = 0;
// Open the file with FileShare.ReadWrite to allow concurrent read/write access // Open the file with FileShare.ReadWrite to allow concurrent read/write access
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
using (var reader = new StreamReader(fileStream, Encoding.UTF8)) using (var reader = new StreamReader(fileStream, Encoding.UTF8))
{ {
while (!reader.EndOfStream) while (!reader.EndOfStream)
@@ -571,7 +571,7 @@ namespace EonaCat.LogStack.Flows
try try
{ {
using (FileStream source = File.OpenRead(encryptedPath)) using (FileStream source = new FileStream(encryptedPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{ {
byte[] magic = new byte[4]; byte[] magic = new byte[4];
ReadExact(source, magic, 4); ReadExact(source, magic, 4);
@@ -604,7 +604,8 @@ namespace EonaCat.LogStack.Flows
} }
using (ICryptoTransform dec = aes.CreateDecryptor()) using (ICryptoTransform dec = aes.CreateDecryptor())
using (StreamWriter out_ = new StreamWriter(outputPath, false, Encoding.UTF8)) using (FileStream output = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
using (StreamWriter out_ = new StreamWriter(output, Encoding.UTF8))
{ {
byte[] buffer = new byte[4]; byte[] buffer = new byte[4];
while (source.Position < source.Length) while (source.Position < source.Length)
@@ -847,7 +848,7 @@ namespace EonaCat.LogStack.Flows
? DateTime.Now.Date ? DateTime.Now.Date
: DateTime.UtcNow.Date; : DateTime.UtcNow.Date;
if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize) if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize || !File.Exists(_currentPath))
{ {
if (_currentStream != null) if (_currentStream != null)
{ {
@@ -934,7 +935,7 @@ namespace EonaCat.LogStack.Flows
bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0; bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0;
_currentStream = new FileStream(_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536, _durableWrites ? FileOptions.WriteThrough : FileOptions.None); _currentStream = new FileStream(_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete, 65536, _durableWrites ? FileOptions.WriteThrough : FileOptions.None);
byte[] salt = new byte[SaltSize]; byte[] salt = new byte[SaltSize];
byte[] iv = new byte[IvSize]; byte[] iv = new byte[IvSize];
@@ -956,7 +957,7 @@ namespace EonaCat.LogStack.Flows
else else
{ {
// Re-read header so we continue the same key/IV session // Re-read header so we continue the same key/IV session
using (FileStream hdr = File.OpenRead(_currentPath)) using (FileStream hdr = new FileStream(_currentPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{ {
hdr.Seek(4, SeekOrigin.Begin); hdr.Seek(4, SeekOrigin.Begin);
ReadExact(hdr, salt, SaltSize); ReadExact(hdr, salt, SaltSize);
@@ -64,6 +64,7 @@ namespace EonaCat.LogStack.Flows
private readonly BlockingCollection<LogEvent> _queue; private readonly BlockingCollection<LogEvent> _queue;
private readonly ConcurrentQueue<string> _compressionQueue = new ConcurrentQueue<string>(); private readonly ConcurrentQueue<string> _compressionQueue = new ConcurrentQueue<string>();
private readonly object _archiveCompressionLock = new object();
private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private volatile bool _isDisposing; // Track disposal state to prevent accessing disposed objects private volatile bool _isDisposing; // Track disposal state to prevent accessing disposed objects
private readonly Thread _writerThread; private readonly Thread _writerThread;
@@ -1385,7 +1386,7 @@ namespace EonaCat.LogStack.Flows
const string footer = "</logs>"; const string footer = "</logs>";
const int tailSize = 512; const int tailSize = 512;
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete))
{ {
if (fs.Length < footer.Length) if (fs.Length < footer.Length)
{ {
@@ -1801,62 +1802,66 @@ namespace EonaCat.LogStack.Flows
private string ArchiveFile(string filePath) private string ArchiveFile(string filePath)
{ {
try lock (_archiveCompressionLock)
{ {
string dir = Path.GetDirectoryName(filePath); DrainCompressionQueue();
string fileName = Path.GetFileName(filePath); try
int extIdx = fileName.LastIndexOf(_fileExtension, StringComparison.OrdinalIgnoreCase);
string baseName = extIdx >= 0 ? fileName.Substring(0, extIdx) : fileName;
int maxFiles = _retention.MaxRolledFiles > 0 ? _retention.MaxRolledFiles : 999;
for (int i = maxFiles - 1; i >= 1; i--)
{ {
string src = Path.Combine(dir, baseName + "_" + i + _fileExtension); string dir = Path.GetDirectoryName(filePath);
string srcGz = src + ".gz"; string fileName = Path.GetFileName(filePath);
string dst = Path.Combine(dir, baseName + "_" + (i + 1) + _fileExtension); int extIdx = fileName.LastIndexOf(_fileExtension, StringComparison.OrdinalIgnoreCase);
string dstGz = dst + ".gz"; string baseName = extIdx >= 0 ? fileName.Substring(0, extIdx) : fileName;
int maxFiles = _retention.MaxRolledFiles > 0 ? _retention.MaxRolledFiles : 999;
if (File.Exists(srcGz)) for (int i = maxFiles - 1; i >= 1; i--)
{ {
if (File.Exists(dstGz)) string src = Path.Combine(dir, baseName + "_" + i + _fileExtension);
string srcGz = src + ".gz";
string dst = Path.Combine(dir, baseName + "_" + (i + 1) + _fileExtension);
string dstGz = dst + ".gz";
if (File.Exists(srcGz))
{ {
File.Delete(dstGz); if (File.Exists(dstGz))
{
File.Delete(dstGz);
}
File.Move(srcGz, dstGz);
} }
File.Move(srcGz, dstGz);
if (!File.Exists(src))
{
continue;
}
if (File.Exists(dst))
{
File.Delete(dst);
}
File.Move(src, dst);
} }
if (!File.Exists(src)) string archive = Path.Combine(dir, baseName + "_1" + _fileExtension);
if (File.Exists(archive))
{ {
continue; File.Delete(archive);
} }
if (File.Exists(dst)) File.Move(filePath, archive);
if (_compressionFormat != CompressionFormat.None)
{ {
File.Delete(dst); _compressionQueue.Enqueue(archive);
_compressionSignal.Release(1);
} }
File.Move(src, dst);
}
string archive = Path.Combine(dir, baseName + "_1" + _fileExtension); return archive;
if (File.Exists(archive)) }
catch (Exception ex)
{ {
File.Delete(archive); OnException?.Invoke(this, "[FileFlow] Archive error '" + filePath + "': " + ex.Message);
return null;
} }
File.Move(filePath, archive);
if (_compressionFormat != CompressionFormat.None)
{
_compressionQueue.Enqueue(archive);
_compressionSignal.Release(1);
}
return archive;
}
catch (Exception ex)
{
OnException?.Invoke(this, "[FileFlow] Archive error '" + filePath + "': " + ex.Message);
return null;
} }
} }
@@ -1891,12 +1896,15 @@ namespace EonaCat.LogStack.Flows
private void DrainCompressionQueue() private void DrainCompressionQueue()
{ {
while (_compressionQueue.TryDequeue(out string path)) lock (_archiveCompressionLock)
{ {
try { CompressFile(path); } while (_compressionQueue.TryDequeue(out string path))
catch (Exception ex)
{ {
OnException?.Invoke(this, "[FileFlow] Compress error '" + path + "': " + ex.Message); try { CompressFile(path); }
catch (Exception ex)
{
OnException?.Invoke(this, "[FileFlow] Compress error '" + path + "': " + ex.Message);
}
} }
} }
} }
@@ -1919,8 +1927,8 @@ namespace EonaCat.LogStack.Flows
const int bufSize = 65536; const int bufSize = 65536;
byte[] buffer = new byte[bufSize]; byte[] buffer = new byte[bufSize];
using (FileStream src = File.OpenRead(path)) using (FileStream src = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
using (FileStream dst = File.Create(outPath)) using (FileStream dst = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.Delete))
using (GZipStream gz = new GZipStream(dst, CompressionLevel.Optimal)) using (GZipStream gz = new GZipStream(dst, CompressionLevel.Optimal))
{ {
int read; int read;
@@ -2063,54 +2071,58 @@ namespace EonaCat.LogStack.Flows
private void ApplyRetention() private void ApplyRetention()
{ {
try lock (_archiveCompressionLock)
{ {
DirectoryInfo dir = new DirectoryInfo(_directory); DrainCompressionQueue();
if (!dir.Exists) try
{ {
return; DirectoryInfo dir = new DirectoryInfo(_directory);
} if (!dir.Exists)
FileInfo[] files = dir.GetFiles("*" + _fileExtension)
.Concat(dir.GetFiles("*" + _fileExtension + ".gz"))
.OrderByDescending(f => f.LastWriteTimeUtc)
.ToArray();
long totalBytes = 0;
int kept = 0;
int keptCompressed = 0;
foreach (FileInfo f in files)
{
bool isCompressed = f.Extension.Equals(".gz", StringComparison.OrdinalIgnoreCase);
bool tooOld = _retention.MaxAgeDays > 0
&& (DateTime.UtcNow - f.LastWriteTimeUtc).TotalDays > _retention.MaxAgeDays;
bool tooMany = _retention.MaxRolledFiles > 0 && kept >= _retention.MaxRolledFiles;
bool tooManyCompressed = isCompressed
&& _retention.MaxCompressedFiles > 0
&& keptCompressed >= _retention.MaxCompressedFiles;
bool tooLarge = _retention.MaxTotalArchiveBytes > 0
&& totalBytes + f.Length > _retention.MaxTotalArchiveBytes;
bool directoryTooLarge = totalBytes + f.Length > _maxDirectorySize;
if (tooOld || tooMany || tooManyCompressed || tooLarge || directoryTooLarge)
{ {
try { f.Delete(); } catch { /* ignore */ } return;
} }
else
FileInfo[] files = dir.GetFiles("*" + _fileExtension)
.Concat(dir.GetFiles("*" + _fileExtension + ".gz"))
.OrderByDescending(f => f.LastWriteTimeUtc)
.ToArray();
long totalBytes = 0;
int kept = 0;
int keptCompressed = 0;
foreach (FileInfo f in files)
{ {
totalBytes += f.Length; bool isCompressed = f.Extension.Equals(".gz", StringComparison.OrdinalIgnoreCase);
kept++; bool tooOld = _retention.MaxAgeDays > 0
if (isCompressed) && (DateTime.UtcNow - f.LastWriteTimeUtc).TotalDays > _retention.MaxAgeDays;
bool tooMany = _retention.MaxRolledFiles > 0 && kept >= _retention.MaxRolledFiles;
bool tooManyCompressed = isCompressed
&& _retention.MaxCompressedFiles > 0
&& keptCompressed >= _retention.MaxCompressedFiles;
bool tooLarge = _retention.MaxTotalArchiveBytes > 0
&& totalBytes + f.Length > _retention.MaxTotalArchiveBytes;
bool directoryTooLarge = totalBytes + f.Length > _maxDirectorySize;
if (tooOld || tooMany || tooManyCompressed || tooLarge || directoryTooLarge)
{ {
keptCompressed++; try { f.Delete(); } catch { /* ignore */ }
}
else
{
totalBytes += f.Length;
kept++;
if (isCompressed)
{
keptCompressed++;
}
} }
} }
} }
} catch (Exception ex)
catch (Exception ex) {
{ OnException?.Invoke(this, "[FileFlow] Retention error: " + ex.Message);
OnException?.Invoke(this, "[FileFlow] Retention error: " + ex.Message); }
} }
} }
@@ -91,7 +91,7 @@ namespace EonaCat.LogStack.Flows
{ {
var targetFileName = GetCurrentFileName(); var targetFileName = GetCurrentFileName();
if (_currentWriter != null && _currentFileName == targetFileName && !ShouldRotate()) if (_currentWriter != null && _currentFileName == targetFileName && !ShouldRotate() && File.Exists(targetFileName))
{ {
return; return;
} }
@@ -107,7 +107,7 @@ namespace EonaCat.LogStack.Flows
_currentFileName, _currentFileName,
FileMode.Append, FileMode.Append,
FileAccess.Write, FileAccess.Write,
FileShare.Read, FileShare.Read | FileShare.Delete,
4096, 4096,
FileOptions.SequentialScan); FileOptions.SequentialScan);
@@ -294,7 +294,8 @@ namespace ServiceMonitoring
try try
{ {
string logFilePath = Path.Combine(_statusDirectory, "status_log.txt"); string logFilePath = Path.Combine(_statusDirectory, "status_log.txt");
using (var writer = new StreamWriter(logFilePath, append: true)) using (var stream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete))
using (var writer = new StreamWriter(stream))
{ {
writer.WriteLine(statusMessage); writer.WriteLine(statusMessage);
} }
@@ -106,7 +106,7 @@ public sealed class TcpFlow : FlowBase
await EnsureConnectedAsync(cancellationToken); await EnsureConnectedAsync(cancellationToken);
// Send file in chunks // Send file in chunks
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{ {
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int bytesRead; int bytesRead;
@@ -39,7 +39,7 @@ namespace EonaCat.LogStack.Helpers
var testFile = Path.Combine(dirPath, ".permission_test_" + Guid.NewGuid().ToString("N")); var testFile = Path.Combine(dirPath, ".permission_test_" + Guid.NewGuid().ToString("N"));
try try
{ {
using (var fs = File.Create(testFile, 1, FileOptions.DeleteOnClose)) using (var fs = new FileStream(testFile, FileMode.Create, FileAccess.Write, FileShare.Delete, 1, FileOptions.DeleteOnClose))
{ {
fs.WriteByte(0); fs.WriteByte(0);
} }
+5 -1
View File
@@ -698,7 +698,11 @@ namespace EonaCat.LogStack.Server
filePath = $"{basePath}_{idx++}.log"; filePath = $"{basePath}_{idx++}.log";
} }
File.AppendAllText(filePath, logData + Environment.NewLine); using (var stream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read | FileShare.Delete))
using (var writer = new StreamWriter(stream))
{
writer.WriteLine(logData);
}
Interlocked.Increment(ref Metrics.TotalWritten); Interlocked.Increment(ref Metrics.TotalWritten);
LogWritten?.Invoke(logData); LogWritten?.Invoke(logData);
+3
View File
@@ -1083,6 +1083,9 @@ await using var logger = new LogBuilder("Production")
Encrypt sensitive logs with AES encryption and password protection. Encrypt sensitive logs with AES encryption and password protection.
Encrypted log files can be deleted while logging is active, including on Windows.
The next log entry recreates the file with a fresh encryption header; deleted entries are not restored.
### Writing Encrypted Logs ### Writing Encrypted Logs
```csharp ```csharp
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" PrivateAssets="all" />
<ProjectReference Include="..\..\EonaCat.LogStack\EonaCat.LogStack.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,148 @@
using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using Xunit;
namespace EonaCat.LogStack.FileDeletionTests;
public sealed class FileDeletionTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "EonaCat.FileDeletionTests", Guid.NewGuid().ToString("N"));
public FileDeletionTests()
{
Directory.CreateDirectory(_directory);
}
[Fact]
public async Task EncryptedLogCanBeDeletedAndRecreatedWithAValidNewHeader()
{
await using var flow = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites();
await WriteEncryptedAsync(flow, "before-deletion");
string path = Assert.Single(Directory.GetFiles(_directory, "*.eona"));
byte[] originalHeader = ReadHeader(path);
Assert.Throws<IOException>(() => File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete));
File.Delete(path);
Assert.False(File.Exists(path));
await WriteEncryptedAsync(flow, "after-deletion");
Assert.True(File.Exists(path));
Assert.False(originalHeader.SequenceEqual(ReadHeader(path)));
string outputPath = Path.Combine(_directory, "decrypted.txt");
Assert.True(EncryptedFileFlow.DecryptToFile(path, outputPath, "test-password"));
string contents = File.ReadAllText(outputPath);
Assert.Contains("after-deletion", contents);
Assert.DoesNotContain("before-deletion", contents);
Assert.Equal(0, flow.GetTotalErrors());
}
[Fact]
public async Task EncryptedLogCanResumeAnExistingFile()
{
byte[] originalHeader;
string path;
await using (var first = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites())
{
await WriteEncryptedAsync(first, "first-session");
path = Assert.Single(Directory.GetFiles(_directory, "*.eona"));
originalHeader = ReadHeader(path);
}
await using var second = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites();
await WriteEncryptedAsync(second, "second-session");
Assert.Equal(originalHeader, ReadHeader(path));
string outputPath = Path.Combine(_directory, "decrypted.txt");
Assert.True(EncryptedFileFlow.DecryptToFile(path, outputPath, "test-password"));
string contents = File.ReadAllText(outputPath);
Assert.Contains("first-session", contents);
Assert.Contains("second-session", contents);
}
[Fact]
public async Task RotatingLogCanBeDeletedAndRecreated()
{
await using var flow = new RotatingFileFlow(_directory);
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("before-deletion")));
string path = Assert.Single(Directory.GetFiles(_directory));
File.Delete(path);
Assert.False(File.Exists(path));
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("after-deletion")));
string contents = ReadSharedText(path);
Assert.Contains("after-deletion", contents);
Assert.DoesNotContain("before-deletion", contents);
}
[Fact]
public async Task AuditLogCanBeDeletedAndRecreatedWithAValidHashChain()
{
var flow = new AuditFlow(_directory);
try
{
await flow.BlastAsync(CreateEvent("before-deletion"));
Assert.Throws<IOException>(() => File.Open(flow.FilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete));
File.Delete(flow.FilePath);
Assert.False(File.Exists(flow.FilePath));
await flow.BlastAsync(CreateEvent("after-deletion"));
await flow.BlastAsync(CreateEvent("next-entry"));
var liveVerification = AuditFlow.Verify(flow.FilePath);
Assert.True(liveVerification.ok, liveVerification.reason);
}
finally
{
await flow.DisposeAsync();
}
var verification = AuditFlow.Verify(flow.FilePath);
Assert.True(verification.ok, verification.reason);
string contents = File.ReadAllText(flow.FilePath);
Assert.Contains("after-deletion", contents);
Assert.Contains("next-entry", contents);
Assert.DoesNotContain("before-deletion", contents);
}
[Fact]
public async Task PlainLogCanBeDeletedWhileOpen()
{
await using var flow = new FileFlow(_directory);
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("before-deletion")));
Assert.True(SpinWait.SpinUntil(() => flow.GetStats().BytesWritten > 0 || flow.GetTotalErrors() > 0, TimeSpan.FromSeconds(10)), "Plain writer did not complete.");
Assert.Equal(0, flow.GetTotalErrors());
await flow.FlushAsync();
string path = Assert.Single(Directory.GetFiles(_directory, "*.log"));
File.Delete(path);
Assert.False(File.Exists(path));
}
private static LogEvent CreateEvent(string message) => new LogEventBuilder().WithMessage(message).Build();
private static async Task WriteEncryptedAsync(EncryptedFileFlow flow, string message)
{
long previousBytes = flow.GetStats().BytesWritten;
Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent(message)));
Assert.True(SpinWait.SpinUntil(() => flow.GetStats().BytesWritten > previousBytes || flow.GetTotalErrors() > 0, TimeSpan.FromSeconds(10)), "Encrypted writer did not complete.");
Assert.Equal(0, flow.GetTotalErrors());
await flow.FlushAsync();
}
private static byte[] ReadHeader(string path)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
byte[] header = new byte[52];
stream.ReadExactly(header);
return header;
}
private static string ReadSharedText(string path)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
public void Dispose()
{
Directory.Delete(_directory, recursive: true);
}
}