Updated
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||
<PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags>
|
||||
<PackageIconUrl />
|
||||
<Version>1.7.8</Version>
|
||||
<FileVersion>1.7.8</FileVersion>
|
||||
<Version>1.7.9</Version>
|
||||
<FileVersion>1.7.9</FileVersion>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
|
||||
@@ -6,10 +6,8 @@ using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -22,58 +20,53 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
private const int BufferSize = 64 * 1024;
|
||||
private const int ChannelCapacity = 8192;
|
||||
private const int FlushThreshold = 48 * 1024;
|
||||
|
||||
private static readonly UTF8Encoding Utf8 = new(false);
|
||||
|
||||
private readonly Channel<LogMessage> _channel;
|
||||
private readonly Thread _writerThread;
|
||||
|
||||
private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
|
||||
private long _lastFlushTicks = Stopwatch.GetTimestamp();
|
||||
private readonly Task _writerTask;
|
||||
private readonly SemaphoreSlim _flushSemaphore = new(1, 1);
|
||||
|
||||
private string _filePath;
|
||||
private readonly int _maxFileSize;
|
||||
private readonly int _maxRolloverFiles;
|
||||
private readonly bool _encryptionEnabled;
|
||||
|
||||
private readonly Aes _aes;
|
||||
private readonly ICryptoTransform _encryptor;
|
||||
private FileStream _fileStream;
|
||||
private CryptoStream? _cryptoStream;
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
private long _size;
|
||||
|
||||
private readonly Aes? _aes;
|
||||
public event Action<Exception>? OnError;
|
||||
|
||||
public bool IncludeCorrelationId { get; }
|
||||
public bool EnableCategoryRouting { get; }
|
||||
|
||||
private FileStream _stream;
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
private long _size;
|
||||
|
||||
[ThreadStatic]
|
||||
private static StringBuilder? _cachedStringBuilder;
|
||||
|
||||
public bool IsHealthy => _running && _stream != null;
|
||||
|
||||
public string LogFile => _filePath;
|
||||
private volatile bool _running = true;
|
||||
private readonly int _maxRolloverFiles;
|
||||
|
||||
public ELogType MinimumLogLevel { get; set; }
|
||||
private readonly LoggerScopedContext _context = new();
|
||||
|
||||
private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
|
||||
private long _lastFlushTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
||||
{
|
||||
var o = options.Value;
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
|
||||
AppDomain.CurrentDomain.UnhandledException += (s, e) => Dispose();
|
||||
|
||||
var o = options.Value;
|
||||
string primaryDirectory = o.LogDirectory;
|
||||
string fileName = $"{o.FileNamePrefix}_{Environment.MachineName}_{DateTime.UtcNow:yyyyMMdd}.log";
|
||||
|
||||
_filePath = Path.Combine(primaryDirectory, fileName);
|
||||
|
||||
if (!TryInitializePath(primaryDirectory, fileName))
|
||||
{
|
||||
// Fallback to temp path with required format: EonaCat_date.log
|
||||
string tempDirectory = Path.GetTempPath();
|
||||
string fallbackFileName = $"EonaCat_{DateTime.UtcNow:yyyyMMdd}.log";
|
||||
|
||||
if (!TryInitializePath(tempDirectory, fallbackFileName))
|
||||
{
|
||||
_running = false;
|
||||
@@ -83,7 +76,6 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
|
||||
_maxFileSize = o.FileSizeLimit;
|
||||
_maxRolloverFiles = o.MaxRolloverFiles;
|
||||
|
||||
_encryptionEnabled = o.EncryptionKey != null && o.EncryptionIV != null;
|
||||
|
||||
if (_encryptionEnabled)
|
||||
@@ -91,41 +83,21 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
_aes = Aes.Create();
|
||||
_aes.Key = o.EncryptionKey;
|
||||
_aes.IV = o.EncryptionIV;
|
||||
_encryptor = _aes.CreateEncryptor();
|
||||
}
|
||||
|
||||
IncludeCorrelationId = o.IncludeCorrelationId;
|
||||
EnableCategoryRouting = o.EnableCategoryRouting;
|
||||
|
||||
try
|
||||
{
|
||||
_stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan);
|
||||
_size = _stream.Length;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RaiseError(ex);
|
||||
_running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
|
||||
|
||||
_channel = Channel.CreateBounded<LogMessage>(
|
||||
new BoundedChannelOptions(ChannelCapacity)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
});
|
||||
|
||||
_writerThread = new Thread(WriterLoop)
|
||||
_channel = Channel.CreateBounded<LogMessage>(new BoundedChannelOptions(ChannelCapacity)
|
||||
{
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.AboveNormal
|
||||
};
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
});
|
||||
|
||||
_writerThread.Start();
|
||||
_writerTask = Task.Run(WriterLoopAsync);
|
||||
}
|
||||
|
||||
private bool TryInitializePath(string directory, string fileName)
|
||||
@@ -133,19 +105,21 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
string fullPath = Path.Combine(directory, fileName);
|
||||
|
||||
if (!EnsureWritable(fullPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_filePath = fullPath;
|
||||
_fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan | FileOptions.WriteThrough);
|
||||
|
||||
_stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan);
|
||||
_size = _stream.Length;
|
||||
if (_encryptionEnabled && _aes != null)
|
||||
{
|
||||
_cryptoStream = new CryptoStream(_fileStream, _aes.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
}
|
||||
|
||||
_size = _fileStream.Length;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -155,15 +129,13 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal override Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
|
||||
{
|
||||
for (int i = 0; i < messages.Count; i++)
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
var message = messages[i];
|
||||
if (message.Level >= MinimumLogLevel)
|
||||
if (msg.Level >= MinimumLogLevel)
|
||||
{
|
||||
while (!_channel.Writer.TryWrite(message))
|
||||
while (!_channel.Writer.TryWrite(msg))
|
||||
{
|
||||
Thread.SpinWait(1);
|
||||
}
|
||||
@@ -172,172 +144,169 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void WriterLoop()
|
||||
private async Task WriterLoopAsync()
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = _channel.Reader;
|
||||
var spin = new SpinWait();
|
||||
List<LogMessage> batch = new();
|
||||
|
||||
while (_running || reader.Count > 0)
|
||||
{
|
||||
while (reader.TryRead(out var message))
|
||||
batch.Clear();
|
||||
while (reader.TryRead(out var msg))
|
||||
{
|
||||
WriteMessage(message);
|
||||
batch.Add(msg);
|
||||
}
|
||||
|
||||
FlushIfNeededTimed();
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
await WriteBatchAsync(batch);
|
||||
}
|
||||
|
||||
spin.SpinOnce();
|
||||
await Task.Delay(50); // idle wait
|
||||
}
|
||||
|
||||
FlushFinal();
|
||||
await FlushFinalAsync();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void FlushIfNeededTimed()
|
||||
private async Task WriteBatchAsync(IReadOnlyList<LogMessage> batch)
|
||||
{
|
||||
var sb = AcquireStringBuilder();
|
||||
foreach (var msg in batch)
|
||||
{
|
||||
if (IncludeCorrelationId)
|
||||
{
|
||||
var ctx = _context.GetAll();
|
||||
if (ctx.Count > 0)
|
||||
{
|
||||
sb.Append(" [");
|
||||
foreach (var kv in ctx)
|
||||
{
|
||||
sb.Append(kv.Key).Append('=').Append(kv.Value).Append(' ');
|
||||
}
|
||||
|
||||
sb.Length--; // trim
|
||||
sb.Append(']');
|
||||
}
|
||||
}
|
||||
sb.Append(' ').Append(msg.Message).AppendLine();
|
||||
}
|
||||
|
||||
int maxBytes = Utf8.GetMaxByteCount(sb.Length);
|
||||
byte[] rented = ArrayPool<byte>.Shared.Rent(maxBytes);
|
||||
int byteCount = Utf8.GetBytes(sb.ToString(), 0, sb.Length, rented, 0);
|
||||
WriteToBuffer(rented, byteCount);
|
||||
ArrayPool<byte>.Shared.Return(rented, true);
|
||||
ReleaseStringBuilder(sb);
|
||||
|
||||
await FlushIfNeededAsync();
|
||||
}
|
||||
|
||||
private async Task FlushIfNeededAsync()
|
||||
{
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
if (_position >= FlushThreshold || now - _lastFlushTicks >= FlushInterval.Ticks)
|
||||
{
|
||||
await _flushSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await FlushInternalAsync();
|
||||
_lastFlushTicks = now;
|
||||
}
|
||||
finally { _flushSemaphore.Release(); }
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteToBuffer(byte[] data, int length)
|
||||
{
|
||||
if (_position + length > _buffer.Length)
|
||||
{
|
||||
FlushInternalAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(data, 0, _buffer, _position, length);
|
||||
_position += length;
|
||||
_size += length;
|
||||
|
||||
if (_maxFileSize > 0 && _size >= _maxFileSize)
|
||||
{
|
||||
Task.Run(RollFileAsync);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FlushInternalAsync()
|
||||
{
|
||||
if (_position == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nowTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Size-based flush (existing behavior)
|
||||
if (_position >= FlushThreshold)
|
||||
try
|
||||
{
|
||||
FlushInternal();
|
||||
_lastFlushTicks = nowTicks;
|
||||
return;
|
||||
if (_cryptoStream != null)
|
||||
{
|
||||
await _cryptoStream.WriteAsync(_buffer, 0, _position);
|
||||
await _cryptoStream.FlushAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _fileStream.WriteAsync(_buffer, 0, _position);
|
||||
await _fileStream.FlushAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { RaiseError(ex); }
|
||||
|
||||
// Time-based flush (new behavior)
|
||||
if (nowTicks - _lastFlushTicks >= FlushInterval.Ticks)
|
||||
{
|
||||
FlushInternal();
|
||||
_lastFlushTicks = nowTicks;
|
||||
}
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteMessage(LogMessage msg)
|
||||
private async Task FlushFinalAsync()
|
||||
{
|
||||
var sb = AcquireStringBuilder();
|
||||
await FlushInternalAsync();
|
||||
|
||||
if (IncludeCorrelationId)
|
||||
{
|
||||
var ctx = _context.GetAll();
|
||||
var tags = msg.Tags;
|
||||
_cryptoStream?.FlushFinalBlock();
|
||||
|
||||
if (ctx.Count > 0 || (tags?.Length ?? 0) > 0)
|
||||
{
|
||||
sb.Append(" [");
|
||||
|
||||
foreach (var kv in ctx)
|
||||
{
|
||||
sb.Append(kv.Key).Append('=').Append(kv.Value).Append(' ');
|
||||
}
|
||||
|
||||
if (tags != null)
|
||||
{
|
||||
for (int i = 0; i < tags.Length; i++)
|
||||
{
|
||||
sb.Append("tag=").Append(tags[i]).Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing space
|
||||
if (sb[sb.Length - 1] == ' ')
|
||||
{
|
||||
sb.Length--;
|
||||
}
|
||||
|
||||
sb.Append(']');
|
||||
}
|
||||
|
||||
// Ensure correlation id exists
|
||||
var correlationId = _context.Get("CorrelationId");
|
||||
if (correlationId == null)
|
||||
{
|
||||
correlationId = Guid.NewGuid().ToString();
|
||||
_context.Set("CorrelationId", correlationId);
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(' ')
|
||||
.Append(msg.Message)
|
||||
.AppendLine();
|
||||
|
||||
int charCount = sb.Length;
|
||||
int maxBytes = Utf8.GetMaxByteCount(charCount);
|
||||
byte[] rented = ArrayPool<byte>.Shared.Rent(maxBytes);
|
||||
|
||||
int byteCount = Utf8.GetBytes(sb.ToString(), 0, charCount, rented, 0);
|
||||
|
||||
byte[] data = rented;
|
||||
int length = byteCount;
|
||||
|
||||
if (_encryptionEnabled)
|
||||
{
|
||||
data = _encryptor.TransformFinalBlock(rented, 0, byteCount);
|
||||
length = data.Length;
|
||||
ArrayPool<byte>.Shared.Return(rented, true);
|
||||
rented = null;
|
||||
}
|
||||
|
||||
WriteToBuffer(data, length);
|
||||
|
||||
if (_maxFileSize > 0 && _size >= _maxFileSize)
|
||||
{
|
||||
RollFile();
|
||||
}
|
||||
|
||||
if (rented != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(rented, true);
|
||||
}
|
||||
|
||||
ReleaseStringBuilder(sb);
|
||||
await _fileStream.FlushAsync();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteToBuffer(byte[] data, int length)
|
||||
|
||||
private async Task RollFileAsync()
|
||||
{
|
||||
if (length > BufferSize)
|
||||
try
|
||||
{
|
||||
if (_position > 0)
|
||||
await _flushSemaphore.WaitAsync();
|
||||
FlushInternalAsync().GetAwaiter().GetResult();
|
||||
|
||||
_cryptoStream?.Dispose();
|
||||
_fileStream.Dispose();
|
||||
|
||||
string tempArchive = _filePath + ".rolling";
|
||||
File.Move(_filePath, tempArchive);
|
||||
|
||||
// Compression in background
|
||||
await Task.Run(() =>
|
||||
{
|
||||
FlushInternal();
|
||||
string dest = _filePath.Replace(".log", "_1.log.gz");
|
||||
using var input = File.OpenRead(tempArchive);
|
||||
using var output = File.Create(dest);
|
||||
using var gzip = new GZipStream(output, CompressionLevel.Fastest);
|
||||
input.CopyTo(gzip);
|
||||
File.Delete(tempArchive);
|
||||
});
|
||||
|
||||
_fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan);
|
||||
if (_encryptionEnabled && _aes != null)
|
||||
{
|
||||
_cryptoStream = new CryptoStream(_fileStream, _aes.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
}
|
||||
|
||||
WriteDirect(data, length);
|
||||
return;
|
||||
_size = 0;
|
||||
}
|
||||
|
||||
if (_position + length > BufferSize)
|
||||
{
|
||||
FlushInternal();
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(data, 0, _buffer, _position, length);
|
||||
_position += length;
|
||||
_size += length;
|
||||
finally { _flushSemaphore.Release(); }
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static StringBuilder AcquireStringBuilder()
|
||||
{
|
||||
var sb = _cachedStringBuilder;
|
||||
if (sb == null)
|
||||
{
|
||||
sb = new StringBuilder(256);
|
||||
_cachedStringBuilder = sb;
|
||||
}
|
||||
if (sb == null) { sb = new StringBuilder(256); _cachedStringBuilder = sb; }
|
||||
else
|
||||
{
|
||||
sb.Clear();
|
||||
@@ -346,255 +315,41 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||
return sb;
|
||||
}
|
||||
|
||||
private const int MaxBuilderCapacity = 8 * 1024;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ReleaseStringBuilder(StringBuilder sb)
|
||||
{
|
||||
if (sb.Capacity > MaxBuilderCapacity)
|
||||
if (sb.Capacity > 8 * 1024)
|
||||
{
|
||||
_cachedStringBuilder = new StringBuilder(256);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDirect(byte[] data, int length)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_stream == null || !File.Exists(_filePath))
|
||||
{
|
||||
RecreateStream();
|
||||
}
|
||||
|
||||
_stream.Write(data, 0, length);
|
||||
}
|
||||
catch (IOException ioEx) when (!File.Exists(_filePath))
|
||||
{
|
||||
// File was removed while writing, recreate and retry once
|
||||
RecreateStream();
|
||||
_stream.Write(data, 0, length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RaiseError(ex);
|
||||
_running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_size += length;
|
||||
|
||||
if (_maxFileSize > 0 && _size >= _maxFileSize)
|
||||
{
|
||||
RollFile();
|
||||
}
|
||||
}
|
||||
|
||||
private void RecreateStream()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
||||
|
||||
_stream?.Dispose();
|
||||
_stream = new FileStream(
|
||||
_filePath,
|
||||
FileMode.Append,
|
||||
FileAccess.Write,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
4096,
|
||||
FileOptions.WriteThrough | FileOptions.SequentialScan);
|
||||
|
||||
_size = _stream.Length;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RaiseError(ex);
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void FlushInternal()
|
||||
{
|
||||
if (_position == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_stream == null || !File.Exists(_filePath))
|
||||
{
|
||||
RecreateStream();
|
||||
}
|
||||
|
||||
_stream.Write(_buffer, 0, _position);
|
||||
}
|
||||
catch (IOException ioEx) when (!File.Exists(_filePath))
|
||||
{
|
||||
RecreateStream();
|
||||
_stream.Write(_buffer, 0, _position);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RaiseError(ex);
|
||||
_running = false;
|
||||
}
|
||||
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
private void FlushFinal()
|
||||
{
|
||||
FlushInternal();
|
||||
_stream.Flush(true);
|
||||
}
|
||||
|
||||
private readonly object _rollLock = new();
|
||||
|
||||
private void RollFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_rollLock)
|
||||
{
|
||||
FlushInternal();
|
||||
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
|
||||
RotateCompressedFiles();
|
||||
|
||||
string tempArchive = _filePath + ".rolling";
|
||||
|
||||
File.Move(_filePath, tempArchive);
|
||||
|
||||
CompressToIndex(tempArchive, 1);
|
||||
|
||||
_stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan);
|
||||
_size = 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RaiseError(ex);
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
private void RotateCompressedFiles()
|
||||
{
|
||||
int maxFiles = _maxRolloverFiles;
|
||||
|
||||
string directory = Path.GetDirectoryName(_filePath)!;
|
||||
string nameWithoutExt = Path.GetFileNameWithoutExtension(_filePath);
|
||||
string extension = Path.GetExtension(_filePath); // .log
|
||||
|
||||
for (int i = maxFiles; i >= 1; i--)
|
||||
{
|
||||
string current = Path.Combine(directory, $"{nameWithoutExt}_{i}{extension}.gz");
|
||||
|
||||
if (!File.Exists(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == maxFiles)
|
||||
{
|
||||
File.Delete(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
string next = Path.Combine(directory, $"{nameWithoutExt}_{i + 1}{extension}.gz");
|
||||
|
||||
if (File.Exists(next))
|
||||
{
|
||||
File.Delete(next);
|
||||
}
|
||||
|
||||
File.Move(current, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureWritable(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? directory = Path.GetDirectoryName(path);
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// Test write access
|
||||
using (var fs = new FileStream(
|
||||
path,
|
||||
FileMode.Append,
|
||||
FileAccess.Write,
|
||||
FileShare.ReadWrite | FileShare.Delete))
|
||||
{
|
||||
// Just opening is enough to validate permission
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
using var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RaiseError(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void CompressToIndex(string sourceFile, int index)
|
||||
{
|
||||
string directory = Path.GetDirectoryName(_filePath)!;
|
||||
string nameWithoutExtension = Path.GetFileNameWithoutExtension(_filePath);
|
||||
string extension = Path.GetExtension(_filePath); // .log
|
||||
|
||||
// destination uses the same "_index" before .log + add .gz
|
||||
string destination = Path.Combine(directory, $"{nameWithoutExtension}_{index}{extension}.gz");
|
||||
|
||||
using (var input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Fastest))
|
||||
{
|
||||
input.CopyTo(gzip);
|
||||
}
|
||||
|
||||
File.Delete(sourceFile);
|
||||
}
|
||||
|
||||
|
||||
protected override Task OnShutdownFlushAsync()
|
||||
{
|
||||
_running = false;
|
||||
_channel.Writer.Complete();
|
||||
_writerThread.Join();
|
||||
|
||||
ArrayPool<byte>.Shared.Return(_buffer, true);
|
||||
_stream.Dispose();
|
||||
_aes?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
catch (Exception ex) { RaiseError(ex); return false; }
|
||||
}
|
||||
|
||||
private void RaiseError(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnError?.Invoke(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never allow logging failure to crash app
|
||||
}
|
||||
try { OnError?.Invoke(ex); } catch { }
|
||||
}
|
||||
|
||||
public new void Dispose()
|
||||
protected override async Task OnShutdownFlushAsync()
|
||||
{
|
||||
OnShutdownFlushAsync().GetAwaiter().GetResult();
|
||||
base.Dispose();
|
||||
_running = false;
|
||||
_channel.Writer.Complete();
|
||||
await _writerTask;
|
||||
|
||||
ArrayPool<byte>.Shared.Return(_buffer, true);
|
||||
_cryptoStream?.Dispose();
|
||||
_fileStream.Dispose();
|
||||
_aes?.Dispose();
|
||||
}
|
||||
|
||||
public new void Dispose() => OnShutdownFlushAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user