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.

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