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.

312 lines
11 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.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. logQueue.Add(new LogMessage
  134. {
  135. Level = level,
  136. Message = message,
  137. Logger = this,
  138. Time = DateTime.Now
  139. });
  140. }
  141. /// <inheritdoc />
  142. /// <summary>
  143. /// An override to <see cref="M:IPA.Logging.Logger.Debug(System.String)" /> which shows the method that called it.
  144. /// </summary>
  145. /// <param name="message">the message to log</param>
  146. public override void Debug(string message)
  147. {
  148. // add source to message
  149. var stackFrame = new StackTrace(true).GetFrame(1);
  150. var method = stackFrame.GetMethod();
  151. var lineNo = stackFrame.GetFileLineNumber();
  152. var paramString = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName));
  153. base.Debug(showSourceClass
  154. ? $"{{{method.DeclaringType?.FullName}::{method.Name}({paramString}):{lineNo}}} {message}"
  155. : message);
  156. }
  157. private struct LogMessage
  158. {
  159. public Level Level;
  160. public StandardLogger Logger;
  161. public string Message;
  162. public DateTime Time;
  163. }
  164. private static readonly BlockingCollection<LogMessage> logQueue = new BlockingCollection<LogMessage>();
  165. private static Thread logThread;
  166. private static StandardLogger loggerLogger;
  167. /// <summary>
  168. /// The log printer thread for <see cref="StandardLogger"/>.
  169. /// </summary>
  170. private static void LogThread()
  171. {
  172. AppDomain.CurrentDomain.ProcessExit += (sender, args) =>
  173. {
  174. StopLogThread();
  175. };
  176. loggerLogger = new StandardLogger("Log Subsystem");
  177. loggerLogger.printers.Clear();
  178. var started = new HashSet<LogPrinter>();
  179. while (logQueue.TryTake(out var msg, Timeout.Infinite))
  180. {
  181. do
  182. {
  183. var logger = msg.Logger;
  184. IEnumerable<LogPrinter> printers = logger.printers;
  185. do
  186. {
  187. logger = logger.parent;
  188. if (logger != null)
  189. printers = printers.Concat(logger.printers);
  190. } while (logger != null);
  191. foreach (var printer in printers.Concat(defaultPrinters))
  192. {
  193. try
  194. {
  195. if (((byte) msg.Level & (byte) printer.Filter) != 0)
  196. {
  197. if (!started.Contains(printer))
  198. {
  199. printer.StartPrint();
  200. started.Add(printer);
  201. }
  202. printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message);
  203. }
  204. }
  205. catch (Exception e)
  206. {
  207. Console.WriteLine($"printer errored: {e}");
  208. }
  209. }
  210. if (logQueue.Count > 512)
  211. {
  212. loggerLogger.printers.Clear();
  213. printers = new LogPrinter[0];
  214. // clear the queue
  215. while (logQueue.TryTake(out var message))
  216. {
  217. var messageLogger = message.Logger;
  218. printers = printers.Concat(messageLogger.printers);
  219. do
  220. {
  221. messageLogger = messageLogger.parent;
  222. if (messageLogger != null)
  223. printers = printers.Concat(messageLogger.printers);
  224. } while (messageLogger != null);
  225. }
  226. // HashSet-ify to make the elements unique
  227. loggerLogger.printers.AddRange(new HashSet<LogPrinter>(printers));
  228. logQueue.Add(new LogMessage
  229. {
  230. Level = Level.Warning,
  231. Logger = loggerLogger,
  232. Message = "Messages omitted to improve performance",
  233. Time = DateTime.Now
  234. });
  235. }
  236. }
  237. // wait for messages for 500ms before ending the prints
  238. while (logQueue.TryTake(out msg, TimeSpan.FromMilliseconds(500)));
  239. if (logQueue.Count == 0)
  240. {
  241. foreach (var printer in started)
  242. {
  243. try
  244. {
  245. printer.EndPrint();
  246. }
  247. catch (Exception e)
  248. {
  249. Console.WriteLine($"printer errored: {e}");
  250. }
  251. }
  252. started.Clear();
  253. }
  254. }
  255. }
  256. /// <summary>
  257. /// Stops and joins the log printer thread.
  258. /// </summary>
  259. internal static void StopLogThread()
  260. {
  261. logQueue.CompleteAdding();
  262. logThread.Join();
  263. }
  264. }
  265. /// <summary>
  266. /// A class providing extensions for various loggers.
  267. /// </summary>
  268. public static class LoggerExtensions
  269. {
  270. /// <summary>
  271. /// 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.
  272. /// </summary>
  273. /// <param name="logger">the parent <see cref="Logger"/></param>
  274. /// <param name="name">the name of the child</param>
  275. /// <returns>the child logger</returns>
  276. public static Logger GetChildLogger(this Logger logger, string name)
  277. {
  278. if (logger is StandardLogger standardLogger)
  279. return standardLogger.GetChild(name);
  280. throw new InvalidOperationException();
  281. }
  282. }
  283. }