mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
57e737b5d7
- FileLogSink: thread-safe file writer with AutoFlush, FileShare.Read for concurrent read access (tail -f), automatic parent directory creation, full date-time timestamps, IDisposable for graceful shutdown - CompositeLogSink: fan-out to multiple sinks with per-sink exception isolation (one broken sink cannot silence the others), IDisposable propagates to children - SharpEmuLog: ResolveSinkFromEnvironment() reads SHARPEMU_LOG_FILE and creates CompositeLogSink(console + file) when set; Sink setter now disposes the previous IDisposable sink to prevent file handle leaks; Shutdown() flushes and disposes the active sink - Program.cs: Main wrapped in try/finally to guarantee SharpEmuLog.Shutdown() runs on all exit paths (GUI, mitigated child, normal, exception) Co-authored-by: Hermes Atlas <hermesatlas@example.com>
116 lines
3.2 KiB
C#
116 lines
3.2 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace SharpEmu.Logging;
|
|
|
|
/// <summary>
|
|
/// Writes log entries to a file. Thread-safe via an internal lock.
|
|
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
|
|
/// </summary>
|
|
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
|
{
|
|
private readonly object _sync = new();
|
|
private readonly StreamWriter _writer;
|
|
private bool _disposed;
|
|
|
|
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
|
|
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
|
|
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
|
|
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
|
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
var fileStream = new FileStream(
|
|
path,
|
|
append ? FileMode.Append : FileMode.Create,
|
|
FileAccess.Write,
|
|
FileShare.Read,
|
|
bufferSize: 4096,
|
|
FileOptions.SequentialScan);
|
|
_writer = new StreamWriter(fileStream, Encoding.UTF8)
|
|
{
|
|
AutoFlush = true
|
|
};
|
|
|
|
IncludeTimestamp = includeTimestamp;
|
|
}
|
|
|
|
public bool IncludeTimestamp { get; set; }
|
|
|
|
public void Write(in LogEntry entry)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IncludeTimestamp)
|
|
{
|
|
_writer.Write('[');
|
|
_writer.Write(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
|
_writer.Write(']');
|
|
}
|
|
|
|
_writer.Write('[');
|
|
_writer.Write(ToLevelLabel(entry.Level));
|
|
_writer.Write(']');
|
|
_writer.Write('[');
|
|
_writer.Write(entry.Category);
|
|
_writer.Write(']');
|
|
_writer.Write(' ');
|
|
|
|
_writer.Write(entry.SourceFileName);
|
|
if (entry.SourceLine > 0)
|
|
{
|
|
_writer.Write(':');
|
|
_writer.Write(entry.SourceLine);
|
|
}
|
|
|
|
_writer.Write(' ');
|
|
_writer.WriteLine(entry.Message);
|
|
|
|
if (entry.Exception is not null)
|
|
{
|
|
_writer.WriteLine(entry.Exception);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
_writer.Flush();
|
|
_writer.Dispose();
|
|
}
|
|
}
|
|
|
|
private static string ToLevelLabel(LogLevel level) => level switch
|
|
{
|
|
LogLevel.Trace => "TRACE",
|
|
LogLevel.Debug => "DEBUG",
|
|
LogLevel.Info => "INFO",
|
|
LogLevel.Warning => "WARNING",
|
|
LogLevel.Error => "ERROR",
|
|
LogLevel.Critical => "CRITICAL",
|
|
_ => "LOG",
|
|
};
|
|
}
|