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.

352 lines
13 KiB

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