diff --git a/Directory.Packages.props b/Directory.Packages.props
index ae05ff54c..35c98e5a3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -52,4 +52,4 @@
-
\ No newline at end of file
+
diff --git a/Ryujinx.Common/Logging/Logger.cs b/Ryujinx.Common/Logging/Logger.cs
index c1abdba9b..4d48dd48d 100644
--- a/Ryujinx.Common/Logging/Logger.cs
+++ b/Ryujinx.Common/Logging/Logger.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.SystemInterop;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -14,6 +15,8 @@ namespace Ryujinx.Common.Logging
private static readonly List m_LogTargets;
+ private static readonly StdErrAdapter _stdErrAdapter;
+
public static event EventHandler Updated;
public readonly struct Log
@@ -77,7 +80,13 @@ namespace Ryujinx.Common.Logging
{
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}";
@@ -119,6 +128,8 @@ namespace Ryujinx.Common.Logging
Warning = new Log(LogLevel.Warning);
Info = new Log(LogLevel.Info);
Trace = new Log(LogLevel.Trace);
+
+ _stdErrAdapter = new StdErrAdapter();
}
public static void RestartTime()
@@ -164,6 +175,8 @@ namespace Ryujinx.Common.Logging
{
Updated = null;
+ _stdErrAdapter.Dispose();
+
foreach (var target in m_LogTargets)
{
target.Dispose();
diff --git a/Ryujinx.Common/SystemInterop/StdErrAdapter.cs b/Ryujinx.Common/SystemInterop/StdErrAdapter.cs
new file mode 100644
index 000000000..12e240ad3
--- /dev/null
+++ b/Ryujinx.Common/SystemInterop/StdErrAdapter.cs
@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+using System.Runtime.Versioning;
+using System.Threading;
+using Ryujinx.Common.Logging;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ public partial class StdErrAdapter : IDisposable
+ {
+ private bool _disposable = false;
+ private UnixStream _pipeReader;
+ private UnixStream _pipeWriter;
+ private Thread _worker;
+
+ public StdErrAdapter()
+ {
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ RegisterPosix();
+ }
+ }
+
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+ private void RegisterPosix()
+ {
+ const int stdErrFileno = 2;
+
+ (int readFd, int writeFd) = MakePipe();
+ dup2(writeFd, stdErrFileno);
+
+ _pipeReader = new UnixStream(readFd);
+ _pipeWriter = new UnixStream(writeFd);
+
+ _worker = new Thread(EventWorker);
+ _disposable = true;
+ _worker.Start();
+ }
+
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+ private void EventWorker()
+ {
+ TextReader reader = new StreamReader(_pipeReader);
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ Logger.Error?.PrintRawMsg(line);
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposable)
+ {
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ _pipeReader?.Close();
+ _pipeWriter?.Close();
+ }
+
+ _disposable = false;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial int dup2(int fd, int fd2);
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static unsafe partial int pipe(int* pipefd);
+
+ private static unsafe (int, int) MakePipe()
+ {
+ int *pipefd = stackalloc int[2];
+
+ if (pipe(pipefd) == 0)
+ {
+ return (pipefd[0], pipefd[1]);
+ }
+ else
+ {
+ throw new();
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Common/SystemInterop/UnixStream.cs b/Ryujinx.Common/SystemInterop/UnixStream.cs
new file mode 100644
index 000000000..1d6449974
--- /dev/null
+++ b/Ryujinx.Common/SystemInterop/UnixStream.cs
@@ -0,0 +1,155 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Common.SystemInterop
+{
+ [SupportedOSPlatform("linux")]
+ [SupportedOSPlatform("macos")]
+ public partial class UnixStream : Stream, IDisposable
+ {
+ private const int InvalidFd = -1;
+
+ private int _fd;
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial long read(int fd, IntPtr buf, ulong count);
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial long write(int fd, IntPtr buf, ulong count);
+
+ [LibraryImport("libc", SetLastError = true)]
+ private static partial int close(int fd);
+
+ public UnixStream(int fd)
+ {
+ if (InvalidFd == fd)
+ {
+ throw new ArgumentException("Invalid file descriptor");
+ }
+
+ _fd = fd;
+
+ CanRead = read(fd, IntPtr.Zero, 0) != -1;
+ CanWrite = write(fd, IntPtr.Zero, 0) != -1;
+ }
+
+ ~UnixStream()
+ {
+ Close();
+ }
+
+ public override bool CanRead { get; }
+ public override bool CanWrite { get; }
+ public override bool CanSeek => false;
+
+ public override long Length => throw new NotSupportedException();
+
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override unsafe int Read([In, Out] byte[] buffer, int offset, int count)
+ {
+ if (offset < 0 || offset > (buffer.Length - count) || count < 0)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (buffer.Length == 0)
+ {
+ return 0;
+ }
+
+ long r = 0;
+ fixed (byte* buf = &buffer[offset])
+ {
+ do
+ {
+ r = read(_fd, (IntPtr)buf, (ulong)count);
+ } while (ShouldRetry(r));
+ }
+
+ return (int)r;
+ }
+
+ public override unsafe void Write(byte[] buffer, int offset, int count)
+ {
+ if (offset < 0 || offset > (buffer.Length - count) || count < 0)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (buffer.Length == 0)
+ {
+ return;
+ }
+
+ fixed (byte* buf = &buffer[offset])
+ {
+ long r = 0;
+ do {
+ r = write(_fd, (IntPtr)buf, (ulong)count);
+ } while (ShouldRetry(r));
+ }
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Close()
+ {
+ if (_fd == InvalidFd)
+ {
+ return;
+ }
+
+ Flush();
+
+ int r;
+ do {
+ r = close(_fd);
+ } while (ShouldRetry(r));
+
+ _fd = InvalidFd;
+ }
+
+ void IDisposable.Dispose()
+ {
+ Close();
+ }
+
+ private bool ShouldRetry(long r)
+ {
+ if (r == -1)
+ {
+ const int eintr = 4;
+
+ int errno = Marshal.GetLastPInvokeError();
+
+ if (errno == eintr)
+ {
+ return true;
+ }
+
+ throw new SystemException($"Operation failed with error 0x{errno:X}");
+ }
+
+ return false;
+ }
+ }
+}