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.

463 lines
18 KiB

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