mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-16 07:56:12 +08:00
initial commit
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
public sealed class ConsoleLogSink : ISharpEmuLogSink
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
|
||||
public ConsoleLogSink(bool useColors = true, bool includeTimestamp = false)
|
||||
{
|
||||
UseColors = useColors;
|
||||
IncludeTimestamp = includeTimestamp;
|
||||
}
|
||||
|
||||
public bool UseColors { get; set; }
|
||||
|
||||
public bool IncludeTimestamp { get; set; }
|
||||
|
||||
public void Write(in LogEntry entry)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var writer = entry.Level >= LogLevel.Error ? Console.Error : Console.Out;
|
||||
if (IncludeTimestamp)
|
||||
{
|
||||
writer.Write('[');
|
||||
writer.Write(entry.Timestamp.ToString("HH:mm:ss.fff"));
|
||||
writer.Write(']');
|
||||
}
|
||||
|
||||
var levelLabel = ToLevelLabel(entry.Level);
|
||||
WriteLevelSegment(writer, levelLabel, entry.Level);
|
||||
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(' ');
|
||||
WriteMessageSegment(writer, entry.Message);
|
||||
writer.WriteLine();
|
||||
if (entry.Exception is not null)
|
||||
{
|
||||
writer.WriteLine(entry.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToLevelLabel(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
LogLevel.Trace => "TRACE",
|
||||
LogLevel.Debug => "DEBUG",
|
||||
LogLevel.Info => "INFO",
|
||||
LogLevel.Warning => "WARNING",
|
||||
LogLevel.Error => "ERROR",
|
||||
LogLevel.Critical => "CRITICAL",
|
||||
_ => "LOG",
|
||||
};
|
||||
}
|
||||
|
||||
private void WriteLevelSegment(TextWriter writer, string label, LogLevel level)
|
||||
{
|
||||
if (!UseColors)
|
||||
{
|
||||
writer.Write('[');
|
||||
writer.Write(label);
|
||||
writer.Write(']');
|
||||
return;
|
||||
}
|
||||
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = GetLevelColor(level);
|
||||
writer.Write('[');
|
||||
writer.Write(label);
|
||||
writer.Write(']');
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
private static ConsoleColor GetLevelColor(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
LogLevel.Trace => ConsoleColor.DarkGray,
|
||||
LogLevel.Debug => ConsoleColor.Gray,
|
||||
LogLevel.Info => ConsoleColor.Blue,
|
||||
LogLevel.Warning => ConsoleColor.Yellow,
|
||||
LogLevel.Error => ConsoleColor.Red,
|
||||
LogLevel.Critical => ConsoleColor.DarkRed,
|
||||
_ => ConsoleColor.White,
|
||||
};
|
||||
}
|
||||
|
||||
private void WriteMessageSegment(TextWriter writer, string message)
|
||||
{
|
||||
if (!UseColors || !TryGetMessageColor(message, out var messageColor))
|
||||
{
|
||||
writer.Write(message);
|
||||
return;
|
||||
}
|
||||
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = messageColor;
|
||||
writer.Write(message);
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
private static bool TryGetMessageColor(string message, out ConsoleColor color)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
color = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ContainsIgnoreCase(message, "unresolved import") ||
|
||||
ContainsIgnoreCase(message, "unresolved symbol") ||
|
||||
ContainsIgnoreCase(message, "hot_unresolved_imports=") ||
|
||||
ContainsIgnoreCase(message, "unresolved_imports_hit="))
|
||||
{
|
||||
color = ConsoleColor.Red;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ContainsIgnoreCase(message, "syscall") ||
|
||||
ContainsIgnoreCase(message, "UnhandledSyscall"))
|
||||
{
|
||||
color = ConsoleColor.DarkYellow;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ContainsIgnoreCase(message, "Import trace") ||
|
||||
ContainsIgnoreCase(message, "import_stub_return") ||
|
||||
ContainsIgnoreCase(message, "last_import=") ||
|
||||
ContainsIgnoreCase(message, "hot_imports=") ||
|
||||
ContainsIgnoreCase(message, "resolved import"))
|
||||
{
|
||||
color = ConsoleColor.Blue;
|
||||
return true;
|
||||
}
|
||||
|
||||
color = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsIgnoreCase(string text, string value)
|
||||
{
|
||||
return text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
public interface ISharpEmuLogSink
|
||||
{
|
||||
void Write(in LogEntry entry);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
public readonly record struct LogEntry(
|
||||
DateTimeOffset Timestamp,
|
||||
LogLevel Level,
|
||||
string Category,
|
||||
string Message,
|
||||
string SourceFileName,
|
||||
int SourceLine,
|
||||
string SourceMemberName,
|
||||
Exception? Exception = null);
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Trace = 0,
|
||||
|
||||
Debug = 1,
|
||||
|
||||
Info = 2,
|
||||
|
||||
Warning = 3,
|
||||
|
||||
Error = 4,
|
||||
|
||||
Critical = 5,
|
||||
|
||||
None = 6,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
</Project>
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
public static class SharpEmuLog
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, SharpEmuLogger> LoggerByCategory =
|
||||
new(StringComparer.Ordinal);
|
||||
private static readonly object ConfigurationSync = new();
|
||||
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
|
||||
useColors: ResolveColorEnabledFromEnvironment(),
|
||||
includeTimestamp: false);
|
||||
|
||||
public static LogLevel MinimumLevel
|
||||
{
|
||||
get => _minimumLevel;
|
||||
set => _minimumLevel = value;
|
||||
}
|
||||
|
||||
public static ISharpEmuLogSink Sink
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (ConfigurationSync)
|
||||
{
|
||||
return _sink;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
lock (ConfigurationSync)
|
||||
{
|
||||
_sink = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
||||
{
|
||||
if (minimumLevel.HasValue)
|
||||
{
|
||||
_minimumLevel = minimumLevel.Value;
|
||||
}
|
||||
|
||||
if (sink is not null)
|
||||
{
|
||||
Sink = sink;
|
||||
}
|
||||
}
|
||||
|
||||
public static SharpEmuLogger For(string category)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||
return LoggerByCategory.GetOrAdd(category, static key => new SharpEmuLogger(key));
|
||||
}
|
||||
|
||||
public static bool TryParseLevel(string? text, out LogLevel level)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
level = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = text.Trim();
|
||||
if (Enum.TryParse<LogLevel>(normalized, ignoreCase: true, out level))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(normalized, "warn", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
level = LogLevel.Warning;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(normalized, "fatal", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
level = LogLevel.Critical;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsEnabled(LogLevel level)
|
||||
{
|
||||
var minimum = _minimumLevel;
|
||||
return minimum != LogLevel.None && level >= minimum;
|
||||
}
|
||||
|
||||
internal static void Write(
|
||||
LogLevel level,
|
||||
string category,
|
||||
string message,
|
||||
Exception? exception,
|
||||
string sourceFilePath,
|
||||
int sourceLine,
|
||||
string sourceMemberName)
|
||||
{
|
||||
if (!IsEnabled(level))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new LogEntry(
|
||||
DateTimeOffset.Now,
|
||||
level,
|
||||
category,
|
||||
message,
|
||||
Path.GetFileName(sourceFilePath),
|
||||
sourceLine,
|
||||
sourceMemberName,
|
||||
exception);
|
||||
|
||||
ISharpEmuLogSink sink;
|
||||
lock (ConfigurationSync)
|
||||
{
|
||||
sink = _sink;
|
||||
}
|
||||
|
||||
sink.Write(in entry);
|
||||
}
|
||||
|
||||
private static LogLevel ResolveMinimumLevelFromEnvironment()
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable("SHARPEMU_LOG_LEVEL");
|
||||
return TryParseLevel(raw, out var level) ? level : LogLevel.Info;
|
||||
}
|
||||
|
||||
private static bool ResolveColorEnabledFromEnvironment()
|
||||
{
|
||||
if (Console.IsOutputRedirected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var raw = Environment.GetEnvironmentVariable("SHARPEMU_LOG_NO_COLOR");
|
||||
return !IsTrueLike(raw);
|
||||
}
|
||||
|
||||
private static bool IsTrueLike(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return text.Trim() switch
|
||||
{
|
||||
"1" => true,
|
||||
"true" => true,
|
||||
"TRUE" => true,
|
||||
"yes" => true,
|
||||
"YES" => true,
|
||||
"on" => true,
|
||||
"ON" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
public sealed class SharpEmuLogger
|
||||
{
|
||||
public SharpEmuLogger(string category)
|
||||
{
|
||||
Category = category ?? throw new ArgumentNullException(nameof(category));
|
||||
}
|
||||
|
||||
public string Category { get; }
|
||||
|
||||
public bool IsEnabled(LogLevel level) => SharpEmuLog.IsEnabled(level);
|
||||
|
||||
public void Trace(
|
||||
string message,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Write(LogLevel.Trace, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
public void Debug(
|
||||
string message,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Write(LogLevel.Debug, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
public void Info(
|
||||
string message,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Write(LogLevel.Info, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
public void Warn(
|
||||
string message,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Warning(message, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
public void Warning(
|
||||
string message,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Write(LogLevel.Warning, message, exception: null, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
public void Error(
|
||||
string message,
|
||||
Exception? exception = null,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Write(LogLevel.Error, message, exception, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
public void Critical(
|
||||
string message,
|
||||
Exception? exception = null,
|
||||
[CallerFilePath] string sourceFilePath = "",
|
||||
[CallerLineNumber] int sourceLine = 0,
|
||||
[CallerMemberName] string sourceMemberName = "")
|
||||
=> Write(LogLevel.Critical, message, exception, sourceFilePath, sourceLine, sourceMemberName);
|
||||
|
||||
private void Write(
|
||||
LogLevel level,
|
||||
string message,
|
||||
Exception? exception,
|
||||
string sourceFilePath,
|
||||
int sourceLine,
|
||||
string sourceMemberName)
|
||||
{
|
||||
SharpEmuLog.Write(
|
||||
level,
|
||||
Category,
|
||||
message,
|
||||
exception,
|
||||
sourceFilePath,
|
||||
sourceLine,
|
||||
sourceMemberName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user