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.

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