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.

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