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.

407 lines
15 KiB

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