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.

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