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.

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