From 9b1cc2cec6135602efc5dc5afa45ed3db261eb42 Mon Sep 17 00:00:00 2001
From: merry <git@mary.rs>
Date: Sat, 25 Feb 2023 15:07:23 +0000
Subject: [PATCH] Logging: Redirect StdErr into logging system (#4427)

* Logging: Redirect StdErr into logging system

* Remove Mono.Unix

* Apply suggestions from code review

Co-authored-by: riperiperi <rhy3756547@hotmail.com>

* Address comments

---------

Co-authored-by: Mary <thog@protonmail.com>
Co-authored-by: riperiperi <rhy3756547@hotmail.com>
Co-authored-by: Mary <mary@mary.zone>
---
 Directory.Packages.props                      |   2 +-
 Ryujinx.Common/Logging/Logger.cs              |  15 +-
 Ryujinx.Common/SystemInterop/StdErrAdapter.cs |  93 +++++++++++
 Ryujinx.Common/SystemInterop/UnixStream.cs    | 155 ++++++++++++++++++
 4 files changed, 263 insertions(+), 2 deletions(-)
 create mode 100644 Ryujinx.Common/SystemInterop/StdErrAdapter.cs
 create mode 100644 Ryujinx.Common/SystemInterop/UnixStream.cs

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 @@
     <PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-a913199" />
     <PackageVersion Include="XamlNameReferenceGenerator" Version="1.5.1" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
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<ILogTarget> m_LogTargets;
 
+        private static readonly StdErrAdapter _stdErrAdapter;
+
         public static event EventHandler<LogEventArgs> 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;
+        }
+    }
+}