aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Common/Logging
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.Common/Logging')
-rw-r--r--src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs42
-rw-r--r--src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs84
-rw-r--r--src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs7
-rw-r--r--src/Ryujinx.Common/Logging/LogClass.cs76
-rw-r--r--src/Ryujinx.Common/Logging/LogEventArgs.cs23
-rw-r--r--src/Ryujinx.Common/Logging/LogEventArgsJson.cs30
-rw-r--r--src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs9
-rw-r--r--src/Ryujinx.Common/Logging/LogLevel.cs19
-rw-r--r--src/Ryujinx.Common/Logging/Logger.cs224
-rw-r--r--src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs79
-rw-r--r--src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs41
-rw-r--r--src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs55
-rw-r--r--src/Ryujinx.Common/Logging/Targets/ILogTarget.cs11
-rw-r--r--src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs40
14 files changed, 740 insertions, 0 deletions
diff --git a/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
new file mode 100644
index 00000000..28a7d546
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs
@@ -0,0 +1,42 @@
+using System.Text;
+
+namespace Ryujinx.Common.Logging
+{
+ internal class DefaultLogFormatter : ILogFormatter
+ {
+ private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
+
+ public string Format(LogEventArgs args)
+ {
+ StringBuilder sb = StringBuilderPool.Allocate();
+
+ try
+ {
+ sb.Clear();
+
+ sb.Append($@"{args.Time:hh\:mm\:ss\.fff}");
+ sb.Append($" |{args.Level.ToString()[0]}| ");
+
+ if (args.ThreadName != null)
+ {
+ sb.Append(args.ThreadName);
+ sb.Append(' ');
+ }
+
+ sb.Append(args.Message);
+
+ if (args.Data is not null)
+ {
+ sb.Append(' ');
+ DynamicObjectFormatter.Format(sb, args.Data);
+ }
+
+ return sb.ToString();
+ }
+ finally
+ {
+ StringBuilderPool.Release(sb);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
new file mode 100644
index 00000000..5f15cc2a
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs
@@ -0,0 +1,84 @@
+#nullable enable
+using System;
+using System.Reflection;
+using System.Text;
+
+namespace Ryujinx.Common.Logging
+{
+ internal class DynamicObjectFormatter
+ {
+ private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
+
+ public static string? Format(object? dynamicObject)
+ {
+ if (dynamicObject is null)
+ {
+ return null;
+ }
+
+ StringBuilder sb = StringBuilderPool.Allocate();
+
+ try
+ {
+ Format(sb, dynamicObject);
+
+ return sb.ToString();
+ }
+ finally
+ {
+ StringBuilderPool.Release(sb);
+ }
+ }
+
+ public static void Format(StringBuilder sb, object? dynamicObject)
+ {
+ if (dynamicObject is null)
+ {
+ return;
+ }
+
+ PropertyInfo[] props = dynamicObject.GetType().GetProperties();
+
+ sb.Append('{');
+
+ foreach (var prop in props)
+ {
+ sb.Append(prop.Name);
+ sb.Append(": ");
+
+ if (typeof(Array).IsAssignableFrom(prop.PropertyType))
+ {
+ Array? array = (Array?) prop.GetValue(dynamicObject);
+
+ if (array is not null)
+ {
+ foreach (var item in array)
+ {
+ sb.Append(item);
+ sb.Append(", ");
+ }
+
+ if (array.Length > 0)
+ {
+ sb.Remove(sb.Length - 2, 2);
+ }
+ }
+ }
+ else
+ {
+ sb.Append(prop.GetValue(dynamicObject));
+ }
+
+ sb.Append(" ; ");
+ }
+
+ // We remove the final ';' from the string
+ if (props.Length > 0)
+ {
+ sb.Remove(sb.Length - 3, 3);
+ }
+
+ sb.Append('}');
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs b/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs
new file mode 100644
index 00000000..9a55bc6b
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Formatters/ILogFormatter.cs
@@ -0,0 +1,7 @@
+namespace Ryujinx.Common.Logging
+{
+ interface ILogFormatter
+ {
+ string Format(LogEventArgs args);
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs
new file mode 100644
index 00000000..e62676cd
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogClass.cs
@@ -0,0 +1,76 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<LogClass>))]
+ public enum LogClass
+ {
+ Application,
+ Audio,
+ AudioRenderer,
+ Configuration,
+ Cpu,
+ Emulation,
+ FFmpeg,
+ Font,
+ Gpu,
+ Hid,
+ Host1x,
+ Kernel,
+ KernelIpc,
+ KernelScheduler,
+ KernelSvc,
+ Loader,
+ ModLoader,
+ Nvdec,
+ Ptc,
+ Service,
+ ServiceAcc,
+ ServiceAm,
+ ServiceApm,
+ ServiceAudio,
+ ServiceBcat,
+ ServiceBsd,
+ ServiceBtm,
+ ServiceCaps,
+ ServiceFatal,
+ ServiceFriend,
+ ServiceFs,
+ ServiceHid,
+ ServiceIrs,
+ ServiceLdn,
+ ServiceLdr,
+ ServiceLm,
+ ServiceMii,
+ ServiceMm,
+ ServiceMnpp,
+ ServiceNfc,
+ ServiceNfp,
+ ServiceNgct,
+ ServiceNifm,
+ ServiceNim,
+ ServiceNs,
+ ServiceNsd,
+ ServiceNtc,
+ ServiceNv,
+ ServiceOlsc,
+ ServicePctl,
+ ServicePcv,
+ ServicePl,
+ ServicePrepo,
+ ServicePsm,
+ ServicePtm,
+ ServiceSet,
+ ServiceSfdnsres,
+ ServiceSm,
+ ServiceSsl,
+ ServiceSss,
+ ServiceTime,
+ ServiceVi,
+ SurfaceFlinger,
+ TamperMachine,
+ Ui,
+ Vic
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogEventArgs.cs b/src/Ryujinx.Common/Logging/LogEventArgs.cs
new file mode 100644
index 00000000..a27af780
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogEventArgs.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Ryujinx.Common.Logging
+{
+ public class LogEventArgs : EventArgs
+ {
+ public readonly LogLevel Level;
+ public readonly TimeSpan Time;
+ public readonly string ThreadName;
+
+ public readonly string Message;
+ public readonly object Data;
+
+ public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null)
+ {
+ Level = level;
+ Time = time;
+ ThreadName = threadName;
+ Message = message;
+ Data = data;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogEventArgsJson.cs b/src/Ryujinx.Common/Logging/LogEventArgsJson.cs
new file mode 100644
index 00000000..425b9766
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogEventArgsJson.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ internal class LogEventArgsJson
+ {
+ public LogLevel Level { get; }
+ public TimeSpan Time { get; }
+ public string ThreadName { get; }
+
+ public string Message { get; }
+ public string Data { get; }
+
+ [JsonConstructor]
+ public LogEventArgsJson(LogLevel level, TimeSpan time, string threadName, string message, string data = null)
+ {
+ Level = level;
+ Time = time;
+ ThreadName = threadName;
+ Message = message;
+ Data = data;
+ }
+
+ public static LogEventArgsJson FromLogEventArgs(LogEventArgs args)
+ {
+ return new LogEventArgsJson(args.Level, args.Time, args.ThreadName, args.Message, DynamicObjectFormatter.Format(args.Data));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs b/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
new file mode 100644
index 00000000..da21f11e
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ [JsonSerializable(typeof(LogEventArgsJson))]
+ internal partial class LogEventJsonSerializerContext : JsonSerializerContext
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs
new file mode 100644
index 00000000..3786c756
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/LogLevel.cs
@@ -0,0 +1,19 @@
+using Ryujinx.Common.Utilities;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.Common.Logging
+{
+ [JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))]
+ public enum LogLevel
+ {
+ Debug,
+ Stub,
+ Info,
+ Warning,
+ Error,
+ Guest,
+ AccessLog,
+ Notice,
+ Trace
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs
new file mode 100644
index 00000000..4d48dd48
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Logger.cs
@@ -0,0 +1,224 @@
+using Ryujinx.Common.SystemInterop;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Ryujinx.Common.Logging
+{
+ public static class Logger
+ {
+ private static readonly Stopwatch m_Time;
+
+ private static readonly bool[] m_EnabledClasses;
+
+ private static readonly List<ILogTarget> m_LogTargets;
+
+ private static readonly StdErrAdapter _stdErrAdapter;
+
+ public static event EventHandler<LogEventArgs> Updated;
+
+ public readonly struct Log
+ {
+ internal readonly LogLevel Level;
+
+ internal Log(LogLevel level)
+ {
+ Level = level;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintMsg(LogClass logClass, string message)
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, "", message)));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Print(LogClass logClass, string message, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message)));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Print(LogClass logClass, string message, object data, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message), data));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintStub(LogClass logClass, string message = "", [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message)));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintStub(LogClass logClass, object data, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed."), data));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintStub(LogClass logClass, string message, object data, [CallerMemberName] string caller = "")
+ {
+ if (m_EnabledClasses[(int)logClass])
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message), data));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void PrintRawMsg(string message)
+ {
+ Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, message));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static string FormatMessage(LogClass Class, string Caller, string Message) => $"{Class} {Caller}: {Message}";
+ }
+
+ public static Log? Debug { get; private set; }
+ public static Log? Info { get; private set; }
+ public static Log? Warning { get; private set; }
+ public static Log? Error { get; private set; }
+ public static Log? Guest { get; private set; }
+ public static Log? AccessLog { get; private set; }
+ public static Log? Stub { get; private set; }
+ public static Log? Trace { get; private set; }
+ public static Log Notice { get; } // Always enabled
+
+ static Logger()
+ {
+ m_EnabledClasses = new bool[Enum.GetNames<LogClass>().Length];
+
+ for (int index = 0; index < m_EnabledClasses.Length; index++)
+ {
+ m_EnabledClasses[index] = true;
+ }
+
+ m_LogTargets = new List<ILogTarget>();
+
+ m_Time = Stopwatch.StartNew();
+
+ // Logger should log to console by default
+ AddTarget(new AsyncLogTargetWrapper(
+ new ConsoleLogTarget("console"),
+ 1000,
+ AsyncLogTargetOverflowAction.Discard));
+
+ Notice = new Log(LogLevel.Notice);
+
+ // Enable important log levels before configuration is loaded
+ Error = new Log(LogLevel.Error);
+ Warning = new Log(LogLevel.Warning);
+ Info = new Log(LogLevel.Info);
+ Trace = new Log(LogLevel.Trace);
+
+ _stdErrAdapter = new StdErrAdapter();
+ }
+
+ public static void RestartTime()
+ {
+ m_Time.Restart();
+ }
+
+ private static ILogTarget GetTarget(string targetName)
+ {
+ foreach (var target in m_LogTargets)
+ {
+ if (target.Name.Equals(targetName))
+ {
+ return target;
+ }
+ }
+
+ return null;
+ }
+
+ public static void AddTarget(ILogTarget target)
+ {
+ m_LogTargets.Add(target);
+
+ Updated += target.Log;
+ }
+
+ public static void RemoveTarget(string target)
+ {
+ ILogTarget logTarget = GetTarget(target);
+
+ if (logTarget != null)
+ {
+ Updated -= logTarget.Log;
+
+ m_LogTargets.Remove(logTarget);
+
+ logTarget.Dispose();
+ }
+ }
+
+ public static void Shutdown()
+ {
+ Updated = null;
+
+ _stdErrAdapter.Dispose();
+
+ foreach (var target in m_LogTargets)
+ {
+ target.Dispose();
+ }
+
+ m_LogTargets.Clear();
+ }
+
+ public static IReadOnlyCollection<LogLevel> GetEnabledLevels()
+ {
+ var logs = new Log?[] { Debug, Info, Warning, Error, Guest, AccessLog, Stub, Trace };
+ List<LogLevel> levels = new List<LogLevel>(logs.Length);
+ foreach (var log in logs)
+ {
+ if (log.HasValue)
+ {
+ levels.Add(log.Value.Level);
+ }
+ }
+
+ return levels;
+ }
+
+ public static void SetEnable(LogLevel logLevel, bool enabled)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.Debug : Debug = enabled ? new Log(LogLevel.Debug) : new Log?(); break;
+ case LogLevel.Info : Info = enabled ? new Log(LogLevel.Info) : new Log?(); break;
+ case LogLevel.Warning : Warning = enabled ? new Log(LogLevel.Warning) : new Log?(); break;
+ case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : new Log?(); break;
+ case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : new Log?(); break;
+ case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog): new Log?(); break;
+ case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : new Log?(); break;
+ case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : new Log?(); break;
+ default: throw new ArgumentException("Unknown Log Level");
+ }
+ }
+
+ public static void SetEnable(LogClass logClass, bool enabled)
+ {
+ m_EnabledClasses[(int)logClass] = enabled;
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
new file mode 100644
index 00000000..43c62d31
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+
+namespace Ryujinx.Common.Logging
+{
+ public enum AsyncLogTargetOverflowAction
+ {
+ /// <summary>
+ /// Block until there's more room in the queue
+ /// </summary>
+ Block = 0,
+
+ /// <summary>
+ /// Discard the overflowing item
+ /// </summary>
+ Discard = 1
+ }
+
+ public class AsyncLogTargetWrapper : ILogTarget
+ {
+ private ILogTarget _target;
+
+ private Thread _messageThread;
+
+ private BlockingCollection<LogEventArgs> _messageQueue;
+
+ private readonly int _overflowTimeout;
+
+ string ILogTarget.Name { get => _target.Name; }
+
+ public AsyncLogTargetWrapper(ILogTarget target)
+ : this(target, -1, AsyncLogTargetOverflowAction.Block)
+ { }
+
+ public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction)
+ {
+ _target = target;
+ _messageQueue = new BlockingCollection<LogEventArgs>(queueLimit);
+ _overflowTimeout = overflowAction == AsyncLogTargetOverflowAction.Block ? -1 : 0;
+
+ _messageThread = new Thread(() => {
+ while (!_messageQueue.IsCompleted)
+ {
+ try
+ {
+ _target.Log(this, _messageQueue.Take());
+ }
+ catch (InvalidOperationException)
+ {
+ // IOE means that Take() was called on a completed collection.
+ // Some other thread can call CompleteAdding after we pass the
+ // IsCompleted check but before we call Take.
+ // We can simply catch the exception since the loop will break
+ // on the next iteration.
+ }
+ }
+ });
+
+ _messageThread.Name = "Logger.MessageThread";
+ _messageThread.IsBackground = true;
+ _messageThread.Start();
+ }
+
+ public void Log(object sender, LogEventArgs e)
+ {
+ if (!_messageQueue.IsAddingCompleted)
+ {
+ _messageQueue.TryAdd(e, _overflowTimeout);
+ }
+ }
+
+ public void Dispose()
+ {
+ _messageQueue.CompleteAdding();
+ _messageThread.Join();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs
new file mode 100644
index 00000000..7b77c4f2
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/ConsoleLogTarget.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Ryujinx.Common.Logging
+{
+ public class ConsoleLogTarget : ILogTarget
+ {
+ private readonly ILogFormatter _formatter;
+
+ private readonly string _name;
+
+ string ILogTarget.Name { get => _name; }
+
+ private static ConsoleColor GetLogColor(LogLevel level) => level switch {
+ LogLevel.Info => ConsoleColor.White,
+ LogLevel.Warning => ConsoleColor.Yellow,
+ LogLevel.Error => ConsoleColor.Red,
+ LogLevel.Stub => ConsoleColor.DarkGray,
+ LogLevel.Notice => ConsoleColor.Cyan,
+ LogLevel.Trace => ConsoleColor.DarkCyan,
+ _ => ConsoleColor.Gray,
+ };
+
+ public ConsoleLogTarget(string name)
+ {
+ _formatter = new DefaultLogFormatter();
+ _name = name;
+ }
+
+ public void Log(object sender, LogEventArgs args)
+ {
+ Console.ForegroundColor = GetLogColor(args.Level);
+ Console.WriteLine(_formatter.Format(args));
+ Console.ResetColor();
+ }
+
+ public void Dispose()
+ {
+ Console.ResetColor();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
new file mode 100644
index 00000000..24dd6d17
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Common.Logging
+{
+ public class FileLogTarget : ILogTarget
+ {
+ private readonly StreamWriter _logWriter;
+ private readonly ILogFormatter _formatter;
+ private readonly string _name;
+
+ string ILogTarget.Name { get => _name; }
+
+ public FileLogTarget(string path, string name)
+ : this(path, name, FileShare.Read, FileMode.Append)
+ { }
+
+ public FileLogTarget(string path, string name, FileShare fileShare, FileMode fileMode)
+ {
+ // Ensure directory is present
+ DirectoryInfo logDir = new DirectoryInfo(Path.Combine(path, "Logs"));
+ logDir.Create();
+
+ // Clean up old logs, should only keep 3
+ FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray();
+ for (int i = 0; i < files.Length - 2; i++)
+ {
+ files[i].Delete();
+ }
+
+ string version = ReleaseInformation.GetVersion();
+
+ // Get path for the current time
+ path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.log");
+
+ _name = name;
+ _logWriter = new StreamWriter(File.Open(path, fileMode, FileAccess.Write, fileShare));
+ _formatter = new DefaultLogFormatter();
+ }
+
+ public void Log(object sender, LogEventArgs args)
+ {
+ _logWriter.WriteLine(_formatter.Format(args));
+ _logWriter.Flush();
+ }
+
+ public void Dispose()
+ {
+ _logWriter.WriteLine("---- End of Log ----");
+ _logWriter.Flush();
+ _logWriter.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs b/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs
new file mode 100644
index 00000000..d4d26a93
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/ILogTarget.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Ryujinx.Common.Logging
+{
+ public interface ILogTarget : IDisposable
+ {
+ void Log(object sender, LogEventArgs args);
+
+ string Name { get; }
+ }
+}
diff --git a/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
new file mode 100644
index 00000000..06976433
--- /dev/null
+++ b/src/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs
@@ -0,0 +1,40 @@
+using Ryujinx.Common.Utilities;
+using System.IO;
+
+namespace Ryujinx.Common.Logging
+{
+ public class JsonLogTarget : ILogTarget
+ {
+ private Stream _stream;
+ private bool _leaveOpen;
+ private string _name;
+
+ string ILogTarget.Name { get => _name; }
+
+ public JsonLogTarget(Stream stream, string name)
+ {
+ _stream = stream;
+ _name = name;
+ }
+
+ public JsonLogTarget(Stream stream, bool leaveOpen)
+ {
+ _stream = stream;
+ _leaveOpen = leaveOpen;
+ }
+
+ public void Log(object sender, LogEventArgs e)
+ {
+ var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e);
+ JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson);
+ }
+
+ public void Dispose()
+ {
+ if (!_leaveOpen)
+ {
+ _stream.Dispose();
+ }
+ }
+ }
+}