You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

240 lines
9.2 KiB

  1. #nullable enable
  2. using HarmonyLib;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Reflection.Emit;
  7. using System.Text;
  8. using System.Threading;
  9. using static IPA.Logging.Logger;
  10. namespace IPA.Logging
  11. {
  12. internal class StdoutInterceptor : TextWriter
  13. {
  14. public override Encoding Encoding => Encoding.Default;
  15. private bool isStdErr;
  16. public override void Write(char value)
  17. {
  18. Write(value.ToString());
  19. }
  20. private string lineBuffer = "";
  21. private readonly object bufferLock = new();
  22. public override void Write(string value)
  23. {
  24. lock (bufferLock)
  25. { // avoid threading issues
  26. lineBuffer += value;
  27. var parts = lineBuffer.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.None);
  28. for (int i = 0; i < parts.Length; i++)
  29. {
  30. if (i + 1 == parts.Length) // last element
  31. lineBuffer = parts[i];
  32. else
  33. {
  34. var str = parts[i];
  35. if (string.IsNullOrEmpty(str)) continue;
  36. if (!isStdErr && WinConsole.IsInitialized)
  37. str = ConsoleColorToForegroundSet(currentColor) + str;
  38. if (isStdErr)
  39. Logger.stdout.Error(str);
  40. else
  41. Logger.stdout.Info(str);
  42. }
  43. }
  44. }
  45. }
  46. private const ConsoleColor defaultColor = ConsoleColor.Gray;
  47. private ConsoleColor currentColor = defaultColor;
  48. internal static string ConsoleColorToForegroundSet(ConsoleColor col)
  49. {
  50. if (!WinConsole.UseVTEscapes) return "";
  51. string code = "0"; // reset
  52. switch (col)
  53. {
  54. case ConsoleColor.Black:
  55. code = "30";
  56. break;
  57. case ConsoleColor.DarkBlue:
  58. code = "34";
  59. break;
  60. case ConsoleColor.DarkGreen:
  61. code = "32";
  62. break;
  63. case ConsoleColor.DarkCyan:
  64. code = "36";
  65. break;
  66. case ConsoleColor.DarkRed:
  67. code = "31";
  68. break;
  69. case ConsoleColor.DarkMagenta:
  70. code = "35";
  71. break;
  72. case ConsoleColor.DarkYellow:
  73. code = "33";
  74. break;
  75. case ConsoleColor.Gray:
  76. code = "37";
  77. break;
  78. case ConsoleColor.DarkGray:
  79. code = "90"; // literally bright black
  80. break;
  81. case ConsoleColor.Blue:
  82. code = "94";
  83. break;
  84. case ConsoleColor.Green:
  85. code = "92";
  86. break;
  87. case ConsoleColor.Cyan:
  88. code = "96";
  89. break;
  90. case ConsoleColor.Red:
  91. code = "91";
  92. break;
  93. case ConsoleColor.Magenta:
  94. code = "95";
  95. break;
  96. case ConsoleColor.Yellow:
  97. code = "93";
  98. break;
  99. case ConsoleColor.White:
  100. code = "97";
  101. break;
  102. }
  103. return "\x1b[" + code + "m";
  104. }
  105. private static StdoutInterceptor? stdoutInterceptor;
  106. private static StdoutInterceptor? stderrInterceptor;
  107. private static class ConsoleHarmonyPatches
  108. {
  109. public static void Patch(Harmony harmony)
  110. {
  111. var console = typeof(Console);
  112. var resetColor = console.GetMethod("ResetColor");
  113. var foregroundProperty = console.GetProperty("ForegroundColor");
  114. var setFg = foregroundProperty?.GetSetMethod();
  115. var getFg = foregroundProperty?.GetGetMethod();
  116. try
  117. {
  118. if (resetColor != null)
  119. _ = harmony.Patch(resetColor, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchResetColor)));
  120. if (foregroundProperty != null)
  121. {
  122. _ = harmony.Patch(setFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchSetForegroundColor)));
  123. _ = harmony.Patch(getFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), nameof(PatchGetForegroundColor)));
  124. }
  125. }
  126. catch (Exception e)
  127. {
  128. // Harmony might be fucked because of wierdness in Guid.NewGuid, don't let that kill us
  129. Logger.Default.Error("Error installing harmony patches to intercept Console color properties:");
  130. Logger.Default.Error(e);
  131. }
  132. }
  133. public static ConsoleColor GetColor() => stdoutInterceptor!.currentColor;
  134. public static void SetColor(ConsoleColor col) => stdoutInterceptor!.currentColor = col;
  135. public static void ResetColor() => stdoutInterceptor!.currentColor = defaultColor;
  136. public static IEnumerable<CodeInstruction> PatchGetForegroundColor(IEnumerable<CodeInstruction> _)
  137. {
  138. var getColorM = typeof(ConsoleHarmonyPatches).GetMethod("GetColor");
  139. return new[] {
  140. new CodeInstruction(OpCodes.Tailcall),
  141. new CodeInstruction(OpCodes.Call, getColorM),
  142. new CodeInstruction(OpCodes.Ret)
  143. };
  144. }
  145. public static IEnumerable<CodeInstruction> PatchSetForegroundColor(IEnumerable<CodeInstruction> _)
  146. {
  147. var setColorM = typeof(ConsoleHarmonyPatches).GetMethod("SetColor");
  148. return new[] {
  149. new CodeInstruction(OpCodes.Ldarg_0),
  150. new CodeInstruction(OpCodes.Tailcall),
  151. new CodeInstruction(OpCodes.Call, setColorM),
  152. new CodeInstruction(OpCodes.Ret)
  153. };
  154. }
  155. public static IEnumerable<CodeInstruction> PatchResetColor(IEnumerable<CodeInstruction> _)
  156. {
  157. var resetColor = typeof(ConsoleHarmonyPatches).GetMethod("ResetColor");
  158. return new[] {
  159. new CodeInstruction(OpCodes.Tailcall),
  160. new CodeInstruction(OpCodes.Call, resetColor),
  161. new CodeInstruction(OpCodes.Ret)
  162. };
  163. }
  164. }
  165. private static Harmony? harmony;
  166. private static bool usingInterceptor;
  167. public static void Intercept()
  168. {
  169. if (!usingInterceptor)
  170. {
  171. usingInterceptor = true;
  172. EnsureHarmonyLogging();
  173. HarmonyGlobalSettings.DisallowLegacyGlobalUnpatchAll = true;
  174. harmony ??= new Harmony("BSIPA Console Redirector Patcher");
  175. stdoutInterceptor ??= new StdoutInterceptor();
  176. stderrInterceptor ??= new StdoutInterceptor { isStdErr = true };
  177. RedirectConsole();
  178. ConsoleHarmonyPatches.Patch(harmony);
  179. }
  180. }
  181. public static void RedirectConsole()
  182. {
  183. if (usingInterceptor)
  184. {
  185. Console.SetOut(stdoutInterceptor);
  186. Console.SetError(stderrInterceptor);
  187. }
  188. }
  189. private static int harmonyLoggingInited;
  190. // I'm not completely sure this is the best place for this, but whatever
  191. internal static void EnsureHarmonyLogging()
  192. {
  193. if (Interlocked.Exchange(ref harmonyLoggingInited, 1) != 0)
  194. return;
  195. HarmonyLib.Tools.Logger.ChannelFilter = HarmonyLib.Tools.Logger.LogChannel.All & ~HarmonyLib.Tools.Logger.LogChannel.IL;
  196. HarmonyLib.Tools.Logger.MessageReceived += (s, e) =>
  197. {
  198. var msg = e.Message;
  199. var lvl = e.LogChannel switch
  200. {
  201. HarmonyLib.Tools.Logger.LogChannel.None => Level.Notice,
  202. HarmonyLib.Tools.Logger.LogChannel.Info => Level.Trace, // HarmonyX logs a *lot* of Info messages
  203. HarmonyLib.Tools.Logger.LogChannel.IL => Level.Trace,
  204. HarmonyLib.Tools.Logger.LogChannel.Warn => Level.Warning,
  205. HarmonyLib.Tools.Logger.LogChannel.Error => Level.Error,
  206. HarmonyLib.Tools.Logger.LogChannel.Debug => Level.Debug,
  207. HarmonyLib.Tools.Logger.LogChannel.All => Level.Critical,
  208. _ => Level.Critical,
  209. };
  210. Logger.Harmony.Log(lvl, msg);
  211. };
  212. }
  213. }
  214. }