diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 65d00fe5..3c2b3fe3 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -48,6 +48,8 @@ namespace IPA.Injector var arguments = Environment.GetCommandLineArgs(); MaybeInitializeConsole(arguments); + StdoutInterceptorPipes.Initialize(); + SetupLibraryLoading(); EnsureDirectories(); diff --git a/IPA.Loader/Logging/StdoutInterceptor.cs b/IPA.Loader/Logging/StdoutInterceptor.cs index 8ba05d24..086219b1 100644 --- a/IPA.Loader/Logging/StdoutInterceptor.cs +++ b/IPA.Loader/Logging/StdoutInterceptor.cs @@ -114,8 +114,8 @@ namespace IPA.Logging return "\x1b[" + code + "m"; } - private static StdoutInterceptor? stdoutInterceptor; - private static StdoutInterceptor? stderrInterceptor; + internal static StdoutInterceptor? Stdout; + internal static StdoutInterceptor? Stderr; private static class ConsoleHarmonyPatches { @@ -145,9 +145,9 @@ namespace IPA.Logging } } - public static ConsoleColor GetColor() => stdoutInterceptor!.currentColor; - public static void SetColor(ConsoleColor col) => stdoutInterceptor!.currentColor = col; - public static void ResetColor() => stdoutInterceptor!.currentColor = defaultColor; + public static ConsoleColor GetColor() => Stdout!.currentColor; + public static void SetColor(ConsoleColor col) => Stdout!.currentColor = col; + public static void ResetColor() => Stdout!.currentColor = defaultColor; public static IEnumerable PatchGetForegroundColor(IEnumerable _) { @@ -194,8 +194,8 @@ namespace IPA.Logging HarmonyGlobalSettings.DisallowLegacyGlobalUnpatchAll = true; harmony ??= new Harmony("BSIPA Console Redirector Patcher"); - stdoutInterceptor ??= new StdoutInterceptor(); - stderrInterceptor ??= new StdoutInterceptor { isStdErr = true }; + Stdout ??= new StdoutInterceptor(); + Stderr ??= new StdoutInterceptor { isStdErr = true }; RedirectConsole(); ConsoleHarmonyPatches.Patch(harmony); @@ -206,8 +206,9 @@ namespace IPA.Logging { if (usingInterceptor) { - Console.SetOut(stdoutInterceptor); - Console.SetError(stderrInterceptor); + Console.SetOut(Stdout); + Console.SetError(Stderr); + StdoutInterceptorPipes.ShouldRedirectStdHandles = true; } } diff --git a/IPA.Loader/Logging/StdoutInterceptorPipes.cs b/IPA.Loader/Logging/StdoutInterceptorPipes.cs new file mode 100644 index 00000000..a58f3fe3 --- /dev/null +++ b/IPA.Loader/Logging/StdoutInterceptorPipes.cs @@ -0,0 +1,103 @@ +using System; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; + +namespace IPA.Logging +{ + internal static class StdoutInterceptorPipes + { + // Used to ensure the server starts first, as Mono struggles with this simple task. + // Otherwise it would throw a ERROR_PIPE_CONNECTED Win32Exception. + private static readonly ManualResetEvent manualResetEvent = new(false); + + public static bool ShouldRedirectStdHandles; + + public static void Initialize() + { + InitializePipe(STD_OUTPUT_HANDLE); + InitializePipe(STD_ERROR_HANDLE); + } + + private static void InitializePipe(int stdHandle) + { + var pipeName = stdHandle == STD_OUTPUT_HANDLE ? "STD_OUT_PIPE" : "STD_ERR_PIPE"; + var serverThread = InstantiateServerThread(pipeName, stdHandle); + serverThread.Start(); + var clientThread = InstantiateClientThread(pipeName, stdHandle); + clientThread.Start(); + } + + private static Thread InstantiateServerThread(string pipeName, int stdHandle) + { + return new Thread(() => + { + NamedPipeServerStream pipeServer = new(pipeName, PipeDirection.In); + + try + { + // If the client starts first, releases the client thread. + manualResetEvent.Set(); + pipeServer.WaitForConnection(); + var buffer = new byte[1024]; + while (pipeServer.IsConnected) + { + if (ShouldRedirectStdHandles) + { + // Separate method to avoid a BadImageFormatException when accessing StdoutInterceptor early. + // This happens because the Harmony DLL is not loaded at this point. + Redirect(pipeServer, buffer, stdHandle); + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + pipeServer.Close(); + } + }); + } + + private static Thread InstantiateClientThread(string pipeName, int stdHandle) + { + return new Thread(() => + { + NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.Out); + + try + { + // If the client starts first, blocks the client thread. + manualResetEvent.WaitOne(); + pipeClient.Connect(); + SetStdHandle(stdHandle, pipeClient.SafePipeHandle.DangerousGetHandle()); + while (pipeClient.IsConnected) + { + // Keeps the thread alive. + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + pipeClient.Close(); + } + }); + } + + private static void Redirect(NamedPipeServerStream server, byte[] buffer, int stdHandle) + { + var charsRead = server.Read(buffer, 0, buffer.Length); + var interceptor = stdHandle == STD_OUTPUT_HANDLE ? StdoutInterceptor.Stdout : StdoutInterceptor.Stderr; + interceptor!.Write(Encoding.UTF8.GetString(buffer, 0, charsRead)); + } + + [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [ResourceExposure(ResourceScope.Process)] + private static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle); + + private const int STD_OUTPUT_HANDLE = -11; + private const int STD_ERROR_HANDLE = -12; + } +} \ No newline at end of file