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
+259 -31

No files matched your search

+7 -2
View File
@@ -36,7 +36,7 @@ namespace EonaCat.LogStack.Server
_maxLogDirectorySize = maxLogDirectorySize;
}
protected virtual Task ProcessLogAsync(string logData)
protected virtual async Task ProcessLogAsync(string logData)
{
string logsRootDirectory = "logs";
@@ -67,7 +67,12 @@ namespace EonaCat.LogStack.Server
// After processing log, check directory size and clean up if needed
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()
+19
View File
@@ -24,6 +24,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.LogStack.Status", "EonaCat.LogStack.Status\EonaCat.LogStack.Status.csproj", "{34C47EBC-BB59-0A5C-9D93-416E4F3D7816}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{25A49D02-5463-4662-B064-55293976DF0A} = {A930DAA9-4165-776A-E33D-6691CB934F50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B01183F3-D85E-45FB-9749-DA281F465A0F}
EndGlobalSection
@@ -134,7 +134,11 @@ namespace EonaCat.LogStack.Features
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
});
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)
@@ -37,7 +37,7 @@ namespace EonaCat.LogStack.Flows
/// This creates a hash-chain so any deletion or modification of a past
/// 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
/// before the method returns, so <see cref="BlastAsync"/> blocks until
/// the entry is flushed.
@@ -53,8 +53,8 @@ namespace EonaCat.LogStack.Flows
private readonly bool _includeProperties;
private readonly object _writeLock = new object();
private readonly FileStream _stream;
private readonly StreamWriter _writer;
private FileStream _stream;
private StreamWriter _writer;
private long _sequence;
private string _previousHash;
@@ -117,19 +117,23 @@ namespace EonaCat.LogStack.Flows
string date = DateTime.UtcNow.ToString("yyyyMMdd");
_filePath = Path.Combine(directory, $"{filePrefix}_{Environment.MachineName}_{date}.audit");
// Exclusive write access
OpenFile();
}
private void OpenFile()
{
_stream = new FileStream(
_filePath,
FileMode.Append,
FileAccess.Write,
FileShare.Read, // allow external readers, but no other writers
FileShare.Read | FileShare.Delete,
bufferSize: 4096,
FileOptions.WriteThrough); // WriteThrough = no OS cache, hits disk immediately
_writer = new StreamWriter(_stream, Encoding.UTF8) { AutoFlush = true };
// Derive starting hash from the last line already in the file (for continuity)
_previousHash = ReadLastHash(directory, filePrefix, date);
_previousHash = ReadLastHash(_filePath);
_sequence = CountExistingLines(_filePath);
}
@@ -153,7 +157,10 @@ namespace EonaCat.LogStack.Flows
string previousHash = new string('0', HashLength);
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("#"))
{
@@ -252,6 +259,7 @@ namespace EonaCat.LogStack.Flows
public override async ValueTask DisposeAsync()
{
IsEnabled = false;
await base.DisposeAsync().ConfigureAwait(false);
lock (_writeLock)
{
try { _writer.Flush(); } catch { }
@@ -259,13 +267,19 @@ namespace EonaCat.LogStack.Flows
try { _writer.Dispose(); } catch { }
try { _stream.Dispose(); } catch { }
}
await base.DisposeAsync().ConfigureAwait(false);
}
private void WriteEntry(LogEvent log)
{
lock (_writeLock)
{
if (!File.Exists(_filePath))
{
_writer.Dispose();
_stream.Dispose();
OpenFile();
}
long seq = Interlocked.Increment(ref _sequence);
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))
{
return new string('0', HashLength);
@@ -368,7 +380,7 @@ namespace EonaCat.LogStack.Flows
string lastLine = null;
// 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))
{
// Read lines from the file
@@ -402,7 +414,7 @@ namespace EonaCat.LogStack.Flows
long count = 0;
// 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))
{
while (!reader.EndOfStream)
@@ -571,7 +571,7 @@ namespace EonaCat.LogStack.Flows
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];
ReadExact(source, magic, 4);
@@ -604,7 +604,8 @@ namespace EonaCat.LogStack.Flows
}
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];
while (source.Position < source.Length)
@@ -847,7 +848,7 @@ namespace EonaCat.LogStack.Flows
? DateTime.Now.Date
: DateTime.UtcNow.Date;
if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize)
if (_currentStream == null || _currentDate != today || _currentSize > _maxFileSize || !File.Exists(_currentPath))
{
if (_currentStream != null)
{
@@ -934,7 +935,7 @@ namespace EonaCat.LogStack.Flows
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[] iv = new byte[IvSize];
@@ -956,7 +957,7 @@ namespace EonaCat.LogStack.Flows
else
{
// 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);
ReadExact(hdr, salt, SaltSize);
@@ -64,6 +64,7 @@ namespace EonaCat.LogStack.Flows
private readonly BlockingCollection<LogEvent> _queue;
private readonly ConcurrentQueue<string> _compressionQueue = new ConcurrentQueue<string>();
private readonly object _archiveCompressionLock = new object();
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private volatile bool _isDisposing; // Track disposal state to prevent accessing disposed objects
private readonly Thread _writerThread;
@@ -1385,7 +1386,7 @@ namespace EonaCat.LogStack.Flows
const string footer = "</logs>";
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)
{
@@ -1801,6 +1802,9 @@ namespace EonaCat.LogStack.Flows
private string ArchiveFile(string filePath)
{
lock (_archiveCompressionLock)
{
DrainCompressionQueue();
try
{
string dir = Path.GetDirectoryName(filePath);
@@ -1859,6 +1863,7 @@ namespace EonaCat.LogStack.Flows
return null;
}
}
}
private void CompressionThreadBody()
{
@@ -1890,6 +1895,8 @@ namespace EonaCat.LogStack.Flows
}
private void DrainCompressionQueue()
{
lock (_archiveCompressionLock)
{
while (_compressionQueue.TryDequeue(out string path))
{
@@ -1900,6 +1907,7 @@ namespace EonaCat.LogStack.Flows
}
}
}
}
private void CompressFile(string path)
{
@@ -1919,8 +1927,8 @@ namespace EonaCat.LogStack.Flows
const int bufSize = 65536;
byte[] buffer = new byte[bufSize];
using (FileStream src = File.OpenRead(path))
using (FileStream dst = File.Create(outPath))
using (FileStream src = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
using (FileStream dst = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.Delete))
using (GZipStream gz = new GZipStream(dst, CompressionLevel.Optimal))
{
int read;
@@ -2063,6 +2071,9 @@ namespace EonaCat.LogStack.Flows
private void ApplyRetention()
{
lock (_archiveCompressionLock)
{
DrainCompressionQueue();
try
{
DirectoryInfo dir = new DirectoryInfo(_directory);
@@ -2113,6 +2124,7 @@ namespace EonaCat.LogStack.Flows
OnException?.Invoke(this, "[FileFlow] Retention error: " + ex.Message);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private DateTime GetCurrentDate()
@@ -91,7 +91,7 @@ namespace EonaCat.LogStack.Flows
{
var targetFileName = GetCurrentFileName();
if (_currentWriter != null && _currentFileName == targetFileName && !ShouldRotate())
if (_currentWriter != null && _currentFileName == targetFileName && !ShouldRotate() && File.Exists(targetFileName))
{
return;
}
@@ -107,7 +107,7 @@ namespace EonaCat.LogStack.Flows
_currentFileName,
FileMode.Append,
FileAccess.Write,
FileShare.Read,
FileShare.Read | FileShare.Delete,
4096,
FileOptions.SequentialScan);
@@ -294,7 +294,8 @@ namespace ServiceMonitoring
try
{
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);
}
@@ -106,7 +106,7 @@ public sealed class TcpFlow : FlowBase
await EnsureConnectedAsync(cancellationToken);
// 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];
int bytesRead;
@@ -39,7 +39,7 @@ namespace EonaCat.LogStack.Helpers
var testFile = Path.Combine(dirPath, ".permission_test_" + Guid.NewGuid().ToString("N"));
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);
}
+5 -1
View File
@@ -698,7 +698,11 @@ namespace EonaCat.LogStack.Server
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);
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.
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
```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);
}
}