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.

459 lines
18 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. #nullable enable
  2. using IPA.Config;
  3. using IPA.Logging.Printers;
  4. using IPA.Utilities;
  5. using System;
  6. using System.Collections.Concurrent;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Threading;
  12. namespace IPA.Logging
  13. {
  14. /// <summary>
  15. /// The default (and standard) <see cref="Logger"/> implementation.
  16. /// </summary>
  17. /// <remarks>
  18. /// <see cref="StandardLogger"/> uses a multi-threaded approach to logging. All actual I/O is done on another thread,
  19. /// where all messaged are guaranteed to be logged in the order they appeared. It is up to the printers to format them.
  20. ///
  21. /// This logger supports child loggers. Use <see cref="LoggerExtensions.GetChildLogger"/> to safely get a child.
  22. /// The modification of printers on a parent are reflected down the chain.
  23. /// </remarks>
  24. public class StandardLogger : Logger
  25. {
  26. private static readonly List<LogPrinter> defaultPrinters = new()
  27. {
  28. new GlobalLogFilePrinter()
  29. };
  30. static StandardLogger()
  31. {
  32. ConsoleColorSupport();
  33. }
  34. private static bool addedConsolePrinters;
  35. private static bool finalizedDefaultPrinters;
  36. internal static void ConsoleColorSupport()
  37. {
  38. if (!addedConsolePrinters && !finalizedDefaultPrinters && WinConsole.IsInitialized )
  39. {
  40. defaultPrinters.AddRange(new []
  41. {
  42. new ColoredConsolePrinter()
  43. {
  44. Filter = LogLevel.TraceOnly,
  45. Color = ConsoleColor.DarkMagenta,
  46. },
  47. new ColoredConsolePrinter()
  48. {
  49. Filter = LogLevel.DebugOnly,
  50. Color = ConsoleColor.Green,
  51. },
  52. new ColoredConsolePrinter()
  53. {
  54. Filter = LogLevel.InfoOnly,
  55. Color = ConsoleColor.White,
  56. },
  57. new ColoredConsolePrinter()
  58. {
  59. Filter = LogLevel.NoticeOnly,
  60. Color = ConsoleColor.Cyan
  61. },
  62. new ColoredConsolePrinter()
  63. {
  64. Filter = LogLevel.WarningOnly,
  65. Color = ConsoleColor.Yellow,
  66. },
  67. new ColoredConsolePrinter()
  68. {
  69. Filter = LogLevel.ErrorOnly,
  70. Color = ConsoleColor.Red,
  71. },
  72. new ColoredConsolePrinter()
  73. {
  74. Filter = LogLevel.CriticalOnly,
  75. Color = ConsoleColor.Magenta,
  76. }
  77. });
  78. addedConsolePrinters = true;
  79. }
  80. }
  81. /// <summary>
  82. /// The <see cref="TextWriter"/> for writing directly to the console window, or stdout if no window open.
  83. /// </summary>
  84. /// <value>a <see cref="TextWriter"/> for the current primary text output</value>
  85. public static TextWriter ConsoleWriter { get; internal set; } = Console.Out;
  86. /// <summary>
  87. /// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger.
  88. /// </summary>
  89. /// <param name="printer">the printer to add</param>
  90. internal static void AddDefaultPrinter(LogPrinter printer)
  91. {
  92. defaultPrinters.Add(printer);
  93. }
  94. private readonly string logName;
  95. private static bool showSourceClass;
  96. /// <summary>
  97. /// All levels defined by this filter will be sent to loggers. All others will be ignored.
  98. /// </summary>
  99. /// <value>the global filter level</value>
  100. public static LogLevel PrintFilter { get; internal set; } = LogLevel.All;
  101. private static bool showTrace = false;
  102. private static volatile bool syncLogging = false;
  103. private readonly List<LogPrinter> printers = new();
  104. private readonly StandardLogger? parent;
  105. private readonly Dictionary<string, StandardLogger> children = new();
  106. /// <summary>
  107. /// Configures internal debug settings based on the config passed in.
  108. /// </summary>
  109. internal static void Configure()
  110. {
  111. showSourceClass = SelfConfig.Debug_.ShowCallSource_;
  112. PrintFilter = SelfConfig.Debug_.ShowDebug_ ? LogLevel.All : LogLevel.InfoUp;
  113. showTrace = SelfConfig.Debug_.ShowTrace_;
  114. syncLogging = SelfConfig.Debug_.SyncLogging_;
  115. }
  116. private StandardLogger(StandardLogger parent, string subName)
  117. {
  118. logName = $"{parent.logName}/{subName}";
  119. this.parent = parent;
  120. printers = new List<LogPrinter>();
  121. if (SelfConfig.Debug_.CreateModLogs_ && !SelfConfig.Debug_.CondenseModLogs_)
  122. printers.Add(new PluginSubLogPrinter(parent.logName, subName));
  123. if (logThread == null || !logThread.IsAlive)
  124. {
  125. logThread = new Thread(LogThread);
  126. logThread.Start();
  127. }
  128. }
  129. internal StandardLogger(string name)
  130. {
  131. ConsoleColorSupport();
  132. if (!finalizedDefaultPrinters)
  133. {
  134. if (!addedConsolePrinters)
  135. AddDefaultPrinter(new ColorlessConsolePrinter());
  136. finalizedDefaultPrinters = true;
  137. }
  138. logName = name;
  139. if (SelfConfig.Debug_.CreateModLogs_)
  140. printers.Add(new PluginLogFilePrinter(name));
  141. if (logThread == null || !logThread.IsAlive)
  142. {
  143. logThread = new Thread(LogThread);
  144. logThread.Start();
  145. }
  146. }
  147. /// <summary>
  148. /// Gets a child printer with the given name, either constructing a new one or using one that was already made.
  149. /// </summary>
  150. /// <param name="name"></param>
  151. /// <returns>a child <see cref="StandardLogger"/> with the given sub-name</returns>
  152. internal StandardLogger GetChild(string name)
  153. {
  154. if (!children.TryGetValue(name, out var child))
  155. {
  156. child = new StandardLogger(this, name);
  157. children.Add(name, child);
  158. }
  159. return child;
  160. }
  161. /// <summary>
  162. /// Adds a log printer to the logger.
  163. /// </summary>
  164. /// <param name="printer">the printer to add</param>
  165. public void AddPrinter(LogPrinter printer)
  166. {
  167. printers.Add(printer);
  168. }
  169. /// <summary>
  170. /// Logs a specific message at a given level.
  171. /// </summary>
  172. /// <param name="level">the message level</param>
  173. /// <param name="message">the message to log</param>
  174. public override void Log(Level level, string message)
  175. {
  176. if (message == null)
  177. throw new ArgumentNullException(nameof(message));
  178. // FIXME: trace doesn't seem to ever actually appear
  179. if (!showTrace && level == Level.Trace) return;
  180. // make sure that the queue isn't being cleared
  181. logWaitEvent.Wait();
  182. try
  183. {
  184. var sync = syncLogging && !IsOnLoggerThread;
  185. if (sync)
  186. {
  187. threadSync ??= new ManualResetEventSlim();
  188. threadSync.Reset();
  189. }
  190. logQueue.Add(new LogMessage
  191. {
  192. Level = level,
  193. Message = message,
  194. Logger = this,
  195. Time = Utils.CurrentTime(),
  196. Sync = threadSync
  197. });
  198. if (sync) threadSync!.Wait();
  199. }
  200. catch (InvalidOperationException)
  201. {
  202. // the queue has been closed, so we leave it
  203. }
  204. }
  205. [ThreadStatic]
  206. private static ManualResetEventSlim? threadSync;
  207. /// <inheritdoc />
  208. /// <summary>
  209. /// An override to <see cref="M:IPA.Logging.Logger.Debug(System.String)" /> which shows the method that called it.
  210. /// </summary>
  211. /// <param name="message">the message to log</param>
  212. public override void Debug(string message)
  213. {
  214. if (showSourceClass)
  215. {
  216. // add source to message
  217. var stackFrame = new StackTrace(true).GetFrame(1);
  218. var lineNo = stackFrame.GetFileLineNumber();
  219. if (lineNo == 0)
  220. { // no debug info
  221. var method = stackFrame.GetMethod();
  222. var paramString = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName).StrJP());
  223. message = $"{{{method.DeclaringType?.FullName}::{method.Name}({paramString})}} {message}";
  224. }
  225. else
  226. message = $"{{{stackFrame.GetFileName()}:{lineNo}}} {message}";
  227. }
  228. base.Debug(message);
  229. }
  230. private struct LogMessage
  231. {
  232. public Level Level;
  233. public StandardLogger Logger;
  234. public string Message;
  235. public DateTime Time;
  236. public ManualResetEventSlim? Sync;
  237. }
  238. [ThreadStatic]
  239. private static bool? isOnLoggerThread = null;
  240. /// <summary>
  241. /// Whether or not the calling thread is the logger thread.
  242. /// </summary>
  243. /// <value><see langword="true"/> if the current thread is the logger thread, <see langword="false"/> otherwise</value>
  244. public static bool IsOnLoggerThread => isOnLoggerThread ??= Thread.CurrentThread.ManagedThreadId == logThread?.ManagedThreadId;
  245. private static readonly ManualResetEventSlim logWaitEvent = new(true);
  246. private static readonly BlockingCollection<LogMessage> logQueue = new();
  247. private static Thread? logThread;
  248. private static StandardLogger? loggerLogger;
  249. private const int LogCloseTimeout = 250;
  250. /// <summary>
  251. /// The log printer thread for <see cref="StandardLogger"/>.
  252. /// </summary>
  253. private static void LogThread()
  254. {
  255. AppDomain.CurrentDomain.ProcessExit += (sender, args) =>
  256. {
  257. StopLogThread();
  258. };
  259. loggerLogger = new StandardLogger("Log Subsystem");
  260. loggerLogger.printers.Clear(); // don't need a log file for this one
  261. var timeout = TimeSpan.FromMilliseconds(LogCloseTimeout);
  262. try
  263. {
  264. var started = new HashSet<LogPrinter>();
  265. while (logQueue.TryTake(out var msg, Timeout.Infinite))
  266. {
  267. StdoutInterceptor.Intercept(); // only runs once, after the first message is queued
  268. do
  269. {
  270. var logger = msg.Logger;
  271. IEnumerable<LogPrinter> printers = logger.printers;
  272. do
  273. { // aggregate all printers in the inheritance chain
  274. logger = logger.parent;
  275. if (logger != null)
  276. printers = printers.Concat(logger.printers);
  277. } while (logger != null);
  278. foreach (var printer in printers.Concat(defaultPrinters))
  279. {
  280. try
  281. { // print to them all
  282. if (((byte)msg.Level & (byte)printer.Filter) != 0)
  283. {
  284. if (!started.Contains(printer))
  285. { // start printer if not started
  286. printer.StartPrint();
  287. _ = started.Add(printer);
  288. }
  289. // update last use time and print
  290. printer.LastUse = Utils.CurrentTime();
  291. printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message);
  292. }
  293. }
  294. catch (Exception e)
  295. {
  296. // do something sane in the face of an error
  297. Console.WriteLine($"printer errored: {e}");
  298. }
  299. }
  300. msg.Sync?.Set();
  301. var debugConfig = SelfConfig.Instance?.Debug;
  302. if (debugConfig != null && debugConfig.HideMessagesForPerformance
  303. && logQueue.Count > debugConfig.HideLogThreshold)
  304. { // spam filtering (if queue has more than HideLogThreshold elements)
  305. logWaitEvent.Reset(); // pause incoming log requests
  306. // clear loggers for this instance, to print the message to all affected logs
  307. loggerLogger.printers.Clear();
  308. var prints = new HashSet<LogPrinter>();
  309. // clear the queue
  310. while (logQueue.TryTake(out var message))
  311. { // aggregate loggers in the process
  312. var messageLogger = message.Logger;
  313. foreach (var print in messageLogger.printers)
  314. _ = prints.Add(print);
  315. do
  316. {
  317. messageLogger = messageLogger.parent;
  318. if (messageLogger != null)
  319. foreach (var print in messageLogger.printers)
  320. _ = prints.Add(print);
  321. } while (messageLogger != null);
  322. message.Sync?.Set();
  323. }
  324. // print using logging subsystem to all logger printers
  325. loggerLogger.printers.AddRange(prints);
  326. logQueue.Add(new LogMessage
  327. { // manually adding to the queue instead of using Warn() because calls to the logger are suspended here
  328. Level = Level.Warning,
  329. Logger = loggerLogger,
  330. Message = $"{loggerLogger.logName.ToUpper()}: Messages omitted to improve performance",
  331. Time = Utils.CurrentTime()
  332. });
  333. // resume log calls
  334. logWaitEvent.Set();
  335. }
  336. var now = Utils.CurrentTime();
  337. var copy = new List<LogPrinter>(started);
  338. foreach (var printer in copy)
  339. {
  340. // close printer after 500ms from its last use
  341. if (now - printer.LastUse > timeout)
  342. {
  343. try
  344. {
  345. printer.EndPrint();
  346. }
  347. catch (Exception e)
  348. {
  349. Console.WriteLine($"printer errored: {e}");
  350. }
  351. _ = started.Remove(printer);
  352. }
  353. }
  354. }
  355. // wait for messages for 500ms before ending the prints
  356. while (logQueue.TryTake(out msg, timeout));
  357. if (logQueue.Count == 0)
  358. { // when the queue has been empty for 500ms, end all prints
  359. foreach (var printer in started)
  360. {
  361. try
  362. {
  363. printer.EndPrint();
  364. }
  365. catch (Exception e)
  366. {
  367. Console.WriteLine($"printer errored: {e}");
  368. }
  369. }
  370. started.Clear();
  371. }
  372. }
  373. }
  374. catch (InvalidOperationException)
  375. {
  376. }
  377. }
  378. /// <summary>
  379. /// Stops and joins the log printer thread.
  380. /// </summary>
  381. internal static void StopLogThread()
  382. {
  383. logQueue.CompleteAdding();
  384. logThread!.Join();
  385. }
  386. }
  387. /// <summary>
  388. /// A class providing extensions for various loggers.
  389. /// </summary>
  390. public static class LoggerExtensions
  391. {
  392. /// <summary>
  393. /// Gets a child logger, if supported. Currently the only defined and supported logger is <see cref="StandardLogger"/>, and most plugins will only ever receive this anyway.
  394. /// </summary>
  395. /// <param name="logger">the parent <see cref="Logger"/></param>
  396. /// <param name="name">the name of the child</param>
  397. /// <returns>the child logger</returns>
  398. public static Logger GetChildLogger(this Logger logger, string name)
  399. => logger switch
  400. {
  401. StandardLogger l => l.GetChild(name),
  402. _ => throw new InvalidOperationException()
  403. };
  404. }
  405. }