diff --git a/BuildTools b/BuildTools index 1a466117..d1ee2f86 160000 --- a/BuildTools +++ b/BuildTools @@ -1 +1 @@ -Subproject commit 1a4661172c07371760fd8b78ea7a39e5d17641d7 +Subproject commit d1ee2f868935cf0407ba95dd617e618d297dfd86 diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index a9ad37e7..00d22287 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -1,158 +1,158 @@ -// BEGIN: section ignore -using IPA.Logging; -using IPA.Utilities; -using IPA.Config.Stores; -using IPA.Config.Stores.Attributes; -using IPA.Config.Stores.Converters; -// END: section ignore -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace IPA.Config -{ - internal class SelfConfig - { - // This is to allow the doc generation to parse this file and use Newtonsoft to generate a JSON schema - // BEGIN: section ignore - - public static Config LoaderConfig { get; set; } - - public static SelfConfig Instance = new SelfConfig(); - - public static void Load() - { - LoaderConfig = Config.GetConfigFor(IPAName, "json"); - Instance = LoaderConfig.Generated(); - } - - protected internal virtual void OnReload() - { - StandardLogger.Configure(this); - } - - protected internal virtual void Changed() - { - Logger.log.Debug("SelfConfig Changed called"); - } - - public static void ReadCommandLine(string[] args) - { - foreach (var arg in args) - { - switch (arg) - { - case "--debug": - case "--mono-debug": - CommandLineValues.Debug.ShowDebug = true; - CommandLineValues.Debug.ShowCallSource = true; - break; - case "--no-yeet": - CommandLineValues.YeetMods = false; - break; - case "--condense-logs": - CommandLineValues.Debug.CondenseModLogs = true; - break; - case "--no-updates": - CommandLineValues.Updates.AutoCheckUpdates = false; - CommandLineValues.Updates.AutoUpdate = false; - break; - case "--trace": - CommandLineValues.Debug.ShowTrace = true; - break; - } - } - } - - internal const string IPAName = "Beat Saber IPA"; - internal const string IPAVersion = "3.99.99.1"; - - // uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug, - // Debug.CondenseModLogs - internal static SelfConfig CommandLineValues = new SelfConfig(); - - // END: section ignore - - public virtual bool Regenerate { get; set; } = true; - - public class Updates_ - { - public virtual bool AutoUpdate { get; set; } = true; - // LINE: ignore 2 - public static bool AutoUpdate_ => (Instance?.Updates?.AutoUpdate ?? true) - && CommandLineValues.Updates.AutoUpdate; - - public virtual bool AutoCheckUpdates { get; set; } = true; - // LINE: ignore 2 - public static bool AutoCheckUpdates_ => (Instance?.Updates?.AutoCheckUpdates ?? true) - && CommandLineValues.Updates.AutoCheckUpdates; - } - - // LINE: ignore - [NonNullable] - public virtual Updates_ Updates { get; set; } = new Updates_(); - - public class Debug_ - { - public virtual bool ShowCallSource { get; set; } = false; - // LINE: ignore 2 - public static bool ShowCallSource_ => (Instance?.Debug?.ShowCallSource ?? false) - || CommandLineValues.Debug.ShowCallSource; - - public virtual bool ShowDebug { get; set; } = false; - // LINE: ignore 2 - public static bool ShowDebug_ => (Instance?.Debug?.ShowDebug ?? false) - || CommandLineValues.Debug.ShowDebug; - - // This option only takes effect after a full game restart, unless new logs are created again - public virtual bool CondenseModLogs { get; set; } = false; - // LINE: ignore 2 - public static bool CondenseModLogs_ => (Instance?.Debug?.CondenseModLogs ?? false) - || CommandLineValues.Debug.CondenseModLogs; - - public virtual bool ShowHandledErrorStackTraces { get; set; } = false; - // LINE: ignore - public static bool ShowHandledErrorStackTraces_ => Instance?.Debug?.ShowHandledErrorStackTraces ?? false; - - public virtual bool HideMessagesForPerformance { get; set; } = true; - // LINE: ignore - public static bool HideMessagesForPerformance_ => Instance?.Debug?.HideMessagesForPerformance ?? true; - - public virtual int HideLogThreshold { get; set; } = 512; - // LINE: ignore - public static int HideLogThreshold_ => Instance?.Debug?.HideLogThreshold ?? 512; - - public virtual bool ShowTrace { get; set; } = false; - // LINE: ignore 2 - public static bool ShowTrace_ => (Instance?.Debug?.ShowTrace ?? false) - || CommandLineValues.Debug.ShowTrace; - } - - // LINE: ignore - [NonNullable] - public virtual Debug_ Debug { get; set; } = new Debug_(); - - public virtual bool YeetMods { get; set; } = true; - // LINE: ignore 2 - public static bool YeetMods_ => (Instance?.YeetMods ?? true) - && CommandLineValues.YeetMods; - - // LINE: ignore - [NonNullable, UseConverter(typeof(CollectionConverter>))] - public virtual HashSet GameAssemblies { get; set; } = new HashSet - { - // LINE: ignore 5 -#if BeatSaber // provide these defaults only for Beat Saber builds - "MainAssembly.dll", "HMLib.dll", "HMUI.dll", "VRUI.dll" -#else // otherwise specify Assembly-CSharp.dll - "Assembly-CSharp.dll" -#endif - }; - // LINE: ignore - public static HashSet GameAssemblies_ => Instance?.GameAssemblies ?? new HashSet { "Assembly-CSharp.dll" }; - - [JsonProperty(Required = Required.DisallowNull)] // Used for documentation schema generation - public virtual string LastGameVersion { get; set; } = null; - // LINE: ignore - public static string LastGameVersion_ => Instance?.LastGameVersion; - } +// BEGIN: section ignore +using IPA.Logging; +using IPA.Utilities; +using IPA.Config.Stores; +using IPA.Config.Stores.Attributes; +using IPA.Config.Stores.Converters; +// END: section ignore +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace IPA.Config +{ + internal class SelfConfig + { + // This is to allow the doc generation to parse this file and use Newtonsoft to generate a JSON schema + // BEGIN: section ignore + + public static Config LoaderConfig { get; set; } + + public static SelfConfig Instance = new SelfConfig(); + + public static void Load() + { + LoaderConfig = Config.GetConfigFor(IPAName, "json"); + Instance = LoaderConfig.Generated(); + } + + protected internal virtual void OnReload() + { + StandardLogger.Configure(); + } + + protected internal virtual void Changed() + { + Logger.log.Debug("SelfConfig Changed called"); + } + + public static void ReadCommandLine(string[] args) + { + foreach (var arg in args) + { + switch (arg) + { + case "--debug": + case "--mono-debug": + CommandLineValues.Debug.ShowDebug = true; + CommandLineValues.Debug.ShowCallSource = true; + break; + case "--no-yeet": + CommandLineValues.YeetMods = false; + break; + case "--condense-logs": + CommandLineValues.Debug.CondenseModLogs = true; + break; + case "--no-updates": + CommandLineValues.Updates.AutoCheckUpdates = false; + CommandLineValues.Updates.AutoUpdate = false; + break; + case "--trace": + CommandLineValues.Debug.ShowTrace = true; + break; + } + } + } + + internal const string IPAName = "Beat Saber IPA"; + internal const string IPAVersion = "3.99.99.1"; + + // uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug, + // Debug.CondenseModLogs + internal static SelfConfig CommandLineValues = new SelfConfig(); + + // END: section ignore + + public virtual bool Regenerate { get; set; } = true; + + public class Updates_ + { + public virtual bool AutoUpdate { get; set; } = true; + // LINE: ignore 2 + public static bool AutoUpdate_ => (Instance?.Updates?.AutoUpdate ?? true) + && CommandLineValues.Updates.AutoUpdate; + + public virtual bool AutoCheckUpdates { get; set; } = true; + // LINE: ignore 2 + public static bool AutoCheckUpdates_ => (Instance?.Updates?.AutoCheckUpdates ?? true) + && CommandLineValues.Updates.AutoCheckUpdates; + } + + // LINE: ignore + [NonNullable] + public virtual Updates_ Updates { get; set; } = new Updates_(); + + public class Debug_ + { + public virtual bool ShowCallSource { get; set; } = false; + // LINE: ignore 2 + public static bool ShowCallSource_ => (Instance?.Debug?.ShowCallSource ?? false) + || CommandLineValues.Debug.ShowCallSource; + + public virtual bool ShowDebug { get; set; } = false; + // LINE: ignore 2 + public static bool ShowDebug_ => (Instance?.Debug?.ShowDebug ?? false) + || CommandLineValues.Debug.ShowDebug; + + // This option only takes effect after a full game restart, unless new logs are created again + public virtual bool CondenseModLogs { get; set; } = false; + // LINE: ignore 2 + public static bool CondenseModLogs_ => (Instance?.Debug?.CondenseModLogs ?? false) + || CommandLineValues.Debug.CondenseModLogs; + + public virtual bool ShowHandledErrorStackTraces { get; set; } = false; + // LINE: ignore + public static bool ShowHandledErrorStackTraces_ => Instance?.Debug?.ShowHandledErrorStackTraces ?? false; + + public virtual bool HideMessagesForPerformance { get; set; } = true; + // LINE: ignore + public static bool HideMessagesForPerformance_ => Instance?.Debug?.HideMessagesForPerformance ?? true; + + public virtual int HideLogThreshold { get; set; } = 512; + // LINE: ignore + public static int HideLogThreshold_ => Instance?.Debug?.HideLogThreshold ?? 512; + + public virtual bool ShowTrace { get; set; } = false; + // LINE: ignore 2 + public static bool ShowTrace_ => (Instance?.Debug?.ShowTrace ?? false) + || CommandLineValues.Debug.ShowTrace; + } + + // LINE: ignore + [NonNullable] + public virtual Debug_ Debug { get; set; } = new Debug_(); + + public virtual bool YeetMods { get; set; } = true; + // LINE: ignore 2 + public static bool YeetMods_ => (Instance?.YeetMods ?? true) + && CommandLineValues.YeetMods; + + // LINE: ignore + [NonNullable, UseConverter(typeof(CollectionConverter>))] + public virtual HashSet GameAssemblies { get; set; } = new HashSet + { + // LINE: ignore 5 +#if BeatSaber // provide these defaults only for Beat Saber builds + "MainAssembly.dll", "HMLib.dll", "HMUI.dll", "VRUI.dll" +#else // otherwise specify Assembly-CSharp.dll + "Assembly-CSharp.dll" +#endif + }; + // LINE: ignore + public static HashSet GameAssemblies_ => Instance?.GameAssemblies ?? new HashSet { "Assembly-CSharp.dll" }; + + [JsonProperty(Required = Required.DisallowNull)] // Used for documentation schema generation + public virtual string LastGameVersion { get; set; } = null; + // LINE: ignore + public static string LastGameVersion_ => Instance?.LastGameVersion; + } } \ No newline at end of file diff --git a/IPA.Loader/Loader/PluginInitInjector.cs b/IPA.Loader/Loader/PluginInitInjector.cs index 33ed4162..384a2ac3 100644 --- a/IPA.Loader/Loader/PluginInitInjector.cs +++ b/IPA.Loader/Loader/PluginInitInjector.cs @@ -1,147 +1,144 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using IPA.Config; -using IPA.Logging; -using IPA.Utilities; -#if NET3 -using Net3_Proxy; -#endif - -namespace IPA.Loader -{ - /// - /// The type that handles value injecting into a plugin's Init. - /// - public static class PluginInitInjector - { - - /// - /// A typed injector for a plugin's Init method. When registered, called for all associated types. If it returns null, the default for the type will be used. - /// - /// the previous return value of the function, or if never called for plugin. - /// the of the parameter being injected. - /// the for the plugin being loaded. - /// the value to inject into that parameter. - public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta); - - /// - /// Adds an injector to be used when calling future plugins' Init methods. - /// - /// the type of the parameter. - /// the function to call for injection. - public static void AddInjector(Type type, InjectParameter injector) - { - injectors.Add(new TypedInjector(type, injector)); - } - - private struct TypedInjector : IEquatable - { - public Type Type; - public InjectParameter Injector; - - public TypedInjector(Type t, InjectParameter i) - { Type = t; Injector = i; } - - public object Inject(object prev, ParameterInfo info, PluginLoader.PluginMetadata meta) - => Injector(prev, info, meta); - - public bool Equals(TypedInjector other) - => Type == other.Type && Injector == other.Injector; - - public override bool Equals(object obj) - => obj is TypedInjector i && Equals(i); - - - public override int GetHashCode() - => Type.GetHashCode() ^ Injector.GetHashCode(); - - public static bool operator ==(TypedInjector a, TypedInjector b) => a.Equals(b); - public static bool operator !=(TypedInjector a, TypedInjector b) => !a.Equals(b); - } - - private static readonly List injectors = new List - { - new TypedInjector(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), -#pragma warning disable CS0618 // Type or member is obsolete - new TypedInjector(typeof(IModPrefs), (prev, param, meta) => prev ?? new ModPrefs(meta)), -#pragma warning restore CS0618 // Type or member is obsolete - new TypedInjector(typeof(PluginLoader.PluginMetadata), (prev, param, meta) => prev ?? meta), - new TypedInjector(typeof(Config.Config), (prev, param, meta) => - { - if (prev != null) return prev; - return Config.Config.GetConfigFor(meta.Name, param); - }) - }; - - private static int? MatchPriority(Type target, Type source) - { - if (target == source) return int.MaxValue; - if (!target.IsAssignableFrom(source)) return null; - if (!target.IsInterface && !source.IsSubclassOf(target)) return int.MinValue; - - int value = 0; - while (true) - { - if (source == null) return value; - if (target.IsInterface && source.GetInterfaces().Contains(target)) - return value; - else if (target == source) - return value; - else - { - value--; // lower priority - source = source.BaseType; - } - } - } - - internal static void Inject(MethodInfo init, PluginLoader.PluginInfo info) - { - var instance = info.Plugin; - var meta = info.Metadata; - - var initArgs = new List(); - var initParams = init.GetParameters(); - - var previousValues = new Dictionary(injectors.Count); - - foreach (var param in initParams) - { - var paramType = param.ParameterType; - - var value = paramType.GetDefault(); - - var toUse = injectors.Select(i => (inject: i, priority: MatchPriority(paramType, i.Type))) // check match priority, combine it - .Where(t => t.priority != null) // filter null priorities - .Select(t => (t.inject, priority: t.priority.Value)) // remove nullable - .OrderByDescending(t => t.priority) // sort by value - .Select(t => t.inject); // remove priority value - - // this tries injectors in order of closest match by type provided - foreach (var pair in toUse) - { - object prev = null; - if (previousValues.ContainsKey(pair)) - prev = previousValues[pair]; - - var val = pair.Inject(prev, param, meta); - - if (previousValues.ContainsKey(pair)) - previousValues[pair] = val; - else - previousValues.Add(pair, val); - - if (val == null) continue; - value = val; - break; - } - - initArgs.Add(value); - } - - init.Invoke(instance, initArgs.ToArray()); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using IPA.Config; +using IPA.Logging; +using IPA.Utilities; +#if NET3 +using Net3_Proxy; +#endif + +namespace IPA.Loader +{ + /// + /// The type that handles value injecting into a plugin's Init. + /// + public static class PluginInitInjector + { + + /// + /// A typed injector for a plugin's Init method. When registered, called for all associated types. If it returns null, the default for the type will be used. + /// + /// the previous return value of the function, or if never called for plugin. + /// the of the parameter being injected. + /// the for the plugin being loaded. + /// the value to inject into that parameter. + public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta); + + /// + /// Adds an injector to be used when calling future plugins' Init methods. + /// + /// the type of the parameter. + /// the function to call for injection. + public static void AddInjector(Type type, InjectParameter injector) + { + injectors.Add(new TypedInjector(type, injector)); + } + + private struct TypedInjector : IEquatable + { + public Type Type; + public InjectParameter Injector; + + public TypedInjector(Type t, InjectParameter i) + { Type = t; Injector = i; } + + public object Inject(object prev, ParameterInfo info, PluginLoader.PluginMetadata meta) + => Injector(prev, info, meta); + + public bool Equals(TypedInjector other) + => Type == other.Type && Injector == other.Injector; + + public override bool Equals(object obj) + => obj is TypedInjector i && Equals(i); + + + public override int GetHashCode() + => Type.GetHashCode() ^ Injector.GetHashCode(); + + public static bool operator ==(TypedInjector a, TypedInjector b) => a.Equals(b); + public static bool operator !=(TypedInjector a, TypedInjector b) => !a.Equals(b); + } + + private static readonly List injectors = new List + { + new TypedInjector(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), + new TypedInjector(typeof(PluginLoader.PluginMetadata), (prev, param, meta) => prev ?? meta), + new TypedInjector(typeof(Config.Config), (prev, param, meta) => + { + if (prev != null) return prev; + return Config.Config.GetConfigFor(meta.Name, param); + }) + }; + + private static int? MatchPriority(Type target, Type source) + { + if (target == source) return int.MaxValue; + if (!target.IsAssignableFrom(source)) return null; + if (!target.IsInterface && !source.IsSubclassOf(target)) return int.MinValue; + + int value = 0; + while (true) + { + if (source == null) return value; + if (target.IsInterface && source.GetInterfaces().Contains(target)) + return value; + else if (target == source) + return value; + else + { + value--; // lower priority + source = source.BaseType; + } + } + } + + internal static void Inject(MethodInfo init, PluginLoader.PluginInfo info) + { + var instance = info.Plugin; + var meta = info.Metadata; + + var initArgs = new List(); + var initParams = init.GetParameters(); + + var previousValues = new Dictionary(injectors.Count); + + foreach (var param in initParams) + { + var paramType = param.ParameterType; + + var value = paramType.GetDefault(); + + var toUse = injectors.Select(i => (inject: i, priority: MatchPriority(paramType, i.Type))) // check match priority, combine it + .Where(t => t.priority != null) // filter null priorities + .Select(t => (t.inject, priority: t.priority.Value)) // remove nullable + .OrderByDescending(t => t.priority) // sort by value + .Select(t => t.inject); // remove priority value + + // this tries injectors in order of closest match by type provided + foreach (var pair in toUse) + { + object prev = null; + if (previousValues.ContainsKey(pair)) + prev = previousValues[pair]; + + var val = pair.Inject(prev, param, meta); + + if (previousValues.ContainsKey(pair)) + previousValues[pair] = val; + else + previousValues.Add(pair, val); + + if (val == null) continue; + value = val; + break; + } + + initArgs.Add(value); + } + + init.Invoke(instance, initArgs.ToArray()); + } + } +} diff --git a/IPA.Loader/Logging/StandardLogger.cs b/IPA.Loader/Logging/StandardLogger.cs index ea77d469..8291707b 100644 --- a/IPA.Loader/Logging/StandardLogger.cs +++ b/IPA.Loader/Logging/StandardLogger.cs @@ -1,417 +1,416 @@ -using IPA.Config; -using IPA.Logging.Printers; -using IPA.Utilities; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; - -namespace IPA.Logging -{ - /// - /// The default (and standard) implementation. - /// - /// - /// uses a multi-threaded approach to logging. All actual I/O is done on another thread, - /// where all messaged are guaranteed to be logged in the order they appeared. It is up to the printers to format them. - /// - /// This logger supports child loggers. Use to safely get a child. - /// The modification of printers on a parent are reflected down the chain. - /// - public class StandardLogger : Logger - { - private static readonly List defaultPrinters = new List() - { - new GlobalLogFilePrinter() - }; - - static StandardLogger() - { - ConsoleColorSupport(); - } - - private static bool addedConsolePrinters; - private static bool finalizedDefaultPrinters; - internal static void ConsoleColorSupport() - { - if (!addedConsolePrinters && !finalizedDefaultPrinters && WinConsole.IsInitialized ) - { - defaultPrinters.AddRange(new [] - { - new ColoredConsolePrinter() - { - Filter = LogLevel.TraceOnly, - Color = ConsoleColor.DarkMagenta, - }, - new ColoredConsolePrinter() - { - Filter = LogLevel.DebugOnly, - Color = ConsoleColor.Green, - }, - new ColoredConsolePrinter() - { - Filter = LogLevel.InfoOnly, - Color = ConsoleColor.White, - }, - new ColoredConsolePrinter() - { - Filter = LogLevel.NoticeOnly, - Color = ConsoleColor.Cyan - }, - new ColoredConsolePrinter() - { - Filter = LogLevel.WarningOnly, - Color = ConsoleColor.Yellow, - }, - new ColoredConsolePrinter() - { - Filter = LogLevel.ErrorOnly, - Color = ConsoleColor.Red, - }, - new ColoredConsolePrinter() - { - Filter = LogLevel.CriticalOnly, - Color = ConsoleColor.Magenta, - } - }); - - addedConsolePrinters = true; - } - } - - /// - /// The for writing directly to the console window, or stdout if no window open. - /// - /// a for the current primary text output - public static TextWriter ConsoleWriter { get; internal set; } = Console.Out; - - /// - /// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger. - /// - /// the printer to add - internal static void AddDefaultPrinter(LogPrinter printer) - { - defaultPrinters.Add(printer); - } - - private readonly string logName; - private static bool showSourceClass; - - /// - /// All levels defined by this filter will be sent to loggers. All others will be ignored. - /// - /// the global filter level - public static LogLevel PrintFilter { get; set; } = LogLevel.All; - private static bool showTrace = false; - - private readonly List printers = new List(); - private readonly StandardLogger parent; - - private readonly Dictionary children = new Dictionary(); - - /// - /// Configures internal debug settings based on the config passed in. - /// - /// - internal static void Configure(SelfConfig cfg) - { - showSourceClass = SelfConfig.Debug_.ShowCallSource_; - PrintFilter = SelfConfig.Debug_.ShowDebug_ ? LogLevel.All : LogLevel.InfoUp; - showTrace = SelfConfig.Debug_.ShowTrace_; - } - - private StandardLogger(StandardLogger parent, string subName) - { - logName = $"{parent.logName}/{subName}"; - this.parent = parent; - printers = new List(); - if (!SelfConfig.Debug_.CondenseModLogs_) - printers.Add(new PluginSubLogPrinter(parent.logName, subName)); - - if (logThread == null || !logThread.IsAlive) - { - logThread = new Thread(LogThread); - logThread.Start(); - } - } - - internal StandardLogger(string name) - { - ConsoleColorSupport(); - if (!finalizedDefaultPrinters) - { - if (!addedConsolePrinters) - AddDefaultPrinter(new ColorlessConsolePrinter()); - - finalizedDefaultPrinters = true; - } - - logName = name; - printers.Add(new PluginLogFilePrinter(name)); - - if (logThread == null || !logThread.IsAlive) - { - logThread = new Thread(LogThread); - logThread.Start(); - } - } - - /// - /// Gets a child printer with the given name, either constructing a new one or using one that was already made. - /// - /// - /// a child with the given sub-name - internal StandardLogger GetChild(string name) - { - if (!children.TryGetValue(name, out var child)) - { - child = new StandardLogger(this, name); - children.Add(name, child); - } - - return child; - } - - /// - /// Adds a log printer to the logger. - /// - /// the printer to add - public void AddPrinter(LogPrinter printer) - { - printers.Add(printer); - } - - /// - /// Logs a specific message at a given level. - /// - /// the message level - /// the message to log - public override void Log(Level level, string message) - { - if (message == null) - throw new ArgumentNullException(nameof(message)); - - if (!showTrace && level == Level.Trace) return; - - // make sure that the queue isn't being cleared - logWaitEvent.Wait(); - logQueue.Add(new LogMessage - { - Level = level, - Message = message, - Logger = this, - Time = Utils.CurrentTime() - }); - } - - /// - /// - /// An override to which shows the method that called it. - /// - /// the message to log - public override void Debug(string message) - { - if (showSourceClass) - { - // add source to message - var stackFrame = new StackTrace(true).GetFrame(1); - var lineNo = stackFrame.GetFileLineNumber(); - - if (lineNo == 0) - { // no debug info - var method = stackFrame.GetMethod(); - var paramString = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName).StrJP()); - - message = $"{{{method.DeclaringType?.FullName}::{method.Name}({paramString})}} {message}"; - } - else - message = $"{{{stackFrame.GetFileName()}:{lineNo}}} {message}"; - - } - - base.Debug(message); - } - - private struct LogMessage - { - public Level Level; - public StandardLogger Logger; - public string Message; - public DateTime Time; - } - - private static ManualResetEventSlim logWaitEvent = new ManualResetEventSlim(true); - private static readonly BlockingCollection logQueue = new BlockingCollection(); - private static Thread logThread; - - private static StandardLogger loggerLogger; - - private const int LogCloseTimeout = 250; - - /// - /// The log printer thread for . - /// - private static void LogThread() - { - AppDomain.CurrentDomain.ProcessExit += (sender, args) => - { - StopLogThread(); - }; - - loggerLogger = new StandardLogger("Log Subsystem"); - loggerLogger.printers.Clear(); // don't need a log file for this one - - var timeout = TimeSpan.FromMilliseconds(LogCloseTimeout); - - var started = new HashSet(); - while (logQueue.TryTake(out var msg, Timeout.Infinite)) - { - StdoutInterceptor.Intercept(); // only runs once, after the first message is queued - do - { - var logger = msg.Logger; - IEnumerable printers = logger.printers; - do - { // aggregate all printers in the inheritance chain - logger = logger.parent; - if (logger != null) - printers = printers.Concat(logger.printers); - } while (logger != null); - - foreach (var printer in printers.Concat(defaultPrinters)) - { - try - { // print to them all - if (((byte) msg.Level & (byte) printer.Filter) != 0) - { - if (!started.Contains(printer)) - { // start printer if not started - printer.StartPrint(); - started.Add(printer); - } - - // update last use time and print - printer.LastUse = Utils.CurrentTime(); - printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message); - } - } - catch (Exception e) - { - // do something sane in the face of an error - Console.WriteLine($"printer errored: {e}"); - } - } - - var debugConfig = SelfConfig.Instance?.Debug; - - if (debugConfig != null && debugConfig.HideMessagesForPerformance - && logQueue.Count > debugConfig.HideLogThreshold) - { // spam filtering (if queue has more than HideLogThreshold elements) - logWaitEvent.Reset(); // pause incoming log requests - - // clear loggers for this instance, to print the message to all affected logs - loggerLogger.printers.Clear(); - var prints = new HashSet(); - // clear the queue - while (logQueue.TryTake(out var message)) - { // aggregate loggers in the process - var messageLogger = message.Logger; - foreach (var print in messageLogger.printers) - prints.Add(print); - do - { - messageLogger = messageLogger.parent; - if (messageLogger != null) - foreach (var print in messageLogger.printers) - prints.Add(print); - } while (messageLogger != null); - } - - // print using logging subsystem to all logger printers - loggerLogger.printers.AddRange(prints); - logQueue.Add(new LogMessage - { // manually adding to the queue instead of using Warn() because calls to the logger are suspended here - Level = Level.Warning, - Logger = loggerLogger, - Message = $"{loggerLogger.logName.ToUpper()}: Messages omitted to improve performance", - Time = Utils.CurrentTime() - }); - - // resume log calls - logWaitEvent.Set(); - } - - var now = Utils.CurrentTime(); - var copy = new List(started); - foreach (var printer in copy) - { - // close printer after 500ms from its last use - if (now - printer.LastUse > timeout) - { - try - { - printer.EndPrint(); - } - catch (Exception e) - { - Console.WriteLine($"printer errored: {e}"); - } - - started.Remove(printer); - } - } - } - // wait for messages for 500ms before ending the prints - while (logQueue.TryTake(out msg, timeout)); - - if (logQueue.Count == 0) - { // when the queue has been empty for 500ms, end all prints - foreach (var printer in started) - { - try - { - printer.EndPrint(); - } - catch (Exception e) - { - Console.WriteLine($"printer errored: {e}"); - } - } - started.Clear(); - } - } - } - - /// - /// Stops and joins the log printer thread. - /// - internal static void StopLogThread() - { - logQueue.CompleteAdding(); - logThread.Join(); - } - } - - /// - /// A class providing extensions for various loggers. - /// - public static class LoggerExtensions - { - /// - /// Gets a child logger, if supported. Currently the only defined and supported logger is , and most plugins will only ever receive this anyway. - /// - /// the parent - /// the name of the child - /// the child logger - public static Logger GetChildLogger(this Logger logger, string name) - { - if (logger is StandardLogger standardLogger) - return standardLogger.GetChild(name); - - throw new InvalidOperationException(); - } - } +using IPA.Config; +using IPA.Logging.Printers; +using IPA.Utilities; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace IPA.Logging +{ + /// + /// The default (and standard) implementation. + /// + /// + /// uses a multi-threaded approach to logging. All actual I/O is done on another thread, + /// where all messaged are guaranteed to be logged in the order they appeared. It is up to the printers to format them. + /// + /// This logger supports child loggers. Use to safely get a child. + /// The modification of printers on a parent are reflected down the chain. + /// + public class StandardLogger : Logger + { + private static readonly List defaultPrinters = new List() + { + new GlobalLogFilePrinter() + }; + + static StandardLogger() + { + ConsoleColorSupport(); + } + + private static bool addedConsolePrinters; + private static bool finalizedDefaultPrinters; + internal static void ConsoleColorSupport() + { + if (!addedConsolePrinters && !finalizedDefaultPrinters && WinConsole.IsInitialized ) + { + defaultPrinters.AddRange(new [] + { + new ColoredConsolePrinter() + { + Filter = LogLevel.TraceOnly, + Color = ConsoleColor.DarkMagenta, + }, + new ColoredConsolePrinter() + { + Filter = LogLevel.DebugOnly, + Color = ConsoleColor.Green, + }, + new ColoredConsolePrinter() + { + Filter = LogLevel.InfoOnly, + Color = ConsoleColor.White, + }, + new ColoredConsolePrinter() + { + Filter = LogLevel.NoticeOnly, + Color = ConsoleColor.Cyan + }, + new ColoredConsolePrinter() + { + Filter = LogLevel.WarningOnly, + Color = ConsoleColor.Yellow, + }, + new ColoredConsolePrinter() + { + Filter = LogLevel.ErrorOnly, + Color = ConsoleColor.Red, + }, + new ColoredConsolePrinter() + { + Filter = LogLevel.CriticalOnly, + Color = ConsoleColor.Magenta, + } + }); + + addedConsolePrinters = true; + } + } + + /// + /// The for writing directly to the console window, or stdout if no window open. + /// + /// a for the current primary text output + public static TextWriter ConsoleWriter { get; internal set; } = Console.Out; + + /// + /// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger. + /// + /// the printer to add + internal static void AddDefaultPrinter(LogPrinter printer) + { + defaultPrinters.Add(printer); + } + + private readonly string logName; + private static bool showSourceClass; + + /// + /// All levels defined by this filter will be sent to loggers. All others will be ignored. + /// + /// the global filter level + internal static LogLevel PrintFilter { get; set; } = LogLevel.All; + private static bool showTrace = false; + + private readonly List printers = new List(); + private readonly StandardLogger parent; + + private readonly Dictionary children = new Dictionary(); + + /// + /// Configures internal debug settings based on the config passed in. + /// + internal static void Configure() + { + showSourceClass = SelfConfig.Debug_.ShowCallSource_; + PrintFilter = SelfConfig.Debug_.ShowDebug_ ? LogLevel.All : LogLevel.InfoUp; + showTrace = SelfConfig.Debug_.ShowTrace_; + } + + private StandardLogger(StandardLogger parent, string subName) + { + logName = $"{parent.logName}/{subName}"; + this.parent = parent; + printers = new List(); + if (!SelfConfig.Debug_.CondenseModLogs_) + printers.Add(new PluginSubLogPrinter(parent.logName, subName)); + + if (logThread == null || !logThread.IsAlive) + { + logThread = new Thread(LogThread); + logThread.Start(); + } + } + + internal StandardLogger(string name) + { + ConsoleColorSupport(); + if (!finalizedDefaultPrinters) + { + if (!addedConsolePrinters) + AddDefaultPrinter(new ColorlessConsolePrinter()); + + finalizedDefaultPrinters = true; + } + + logName = name; + printers.Add(new PluginLogFilePrinter(name)); + + if (logThread == null || !logThread.IsAlive) + { + logThread = new Thread(LogThread); + logThread.Start(); + } + } + + /// + /// Gets a child printer with the given name, either constructing a new one or using one that was already made. + /// + /// + /// a child with the given sub-name + internal StandardLogger GetChild(string name) + { + if (!children.TryGetValue(name, out var child)) + { + child = new StandardLogger(this, name); + children.Add(name, child); + } + + return child; + } + + /// + /// Adds a log printer to the logger. + /// + /// the printer to add + public void AddPrinter(LogPrinter printer) + { + printers.Add(printer); + } + + /// + /// Logs a specific message at a given level. + /// + /// the message level + /// the message to log + public override void Log(Level level, string message) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + if (!showTrace && level == Level.Trace) return; + + // make sure that the queue isn't being cleared + logWaitEvent.Wait(); + logQueue.Add(new LogMessage + { + Level = level, + Message = message, + Logger = this, + Time = Utils.CurrentTime() + }); + } + + /// + /// + /// An override to which shows the method that called it. + /// + /// the message to log + public override void Debug(string message) + { + if (showSourceClass) + { + // add source to message + var stackFrame = new StackTrace(true).GetFrame(1); + var lineNo = stackFrame.GetFileLineNumber(); + + if (lineNo == 0) + { // no debug info + var method = stackFrame.GetMethod(); + var paramString = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName).StrJP()); + + message = $"{{{method.DeclaringType?.FullName}::{method.Name}({paramString})}} {message}"; + } + else + message = $"{{{stackFrame.GetFileName()}:{lineNo}}} {message}"; + + } + + base.Debug(message); + } + + private struct LogMessage + { + public Level Level; + public StandardLogger Logger; + public string Message; + public DateTime Time; + } + + private static ManualResetEventSlim logWaitEvent = new ManualResetEventSlim(true); + private static readonly BlockingCollection logQueue = new BlockingCollection(); + private static Thread logThread; + + private static StandardLogger loggerLogger; + + private const int LogCloseTimeout = 250; + + /// + /// The log printer thread for . + /// + private static void LogThread() + { + AppDomain.CurrentDomain.ProcessExit += (sender, args) => + { + StopLogThread(); + }; + + loggerLogger = new StandardLogger("Log Subsystem"); + loggerLogger.printers.Clear(); // don't need a log file for this one + + var timeout = TimeSpan.FromMilliseconds(LogCloseTimeout); + + var started = new HashSet(); + while (logQueue.TryTake(out var msg, Timeout.Infinite)) + { + StdoutInterceptor.Intercept(); // only runs once, after the first message is queued + do + { + var logger = msg.Logger; + IEnumerable printers = logger.printers; + do + { // aggregate all printers in the inheritance chain + logger = logger.parent; + if (logger != null) + printers = printers.Concat(logger.printers); + } while (logger != null); + + foreach (var printer in printers.Concat(defaultPrinters)) + { + try + { // print to them all + if (((byte) msg.Level & (byte) printer.Filter) != 0) + { + if (!started.Contains(printer)) + { // start printer if not started + printer.StartPrint(); + started.Add(printer); + } + + // update last use time and print + printer.LastUse = Utils.CurrentTime(); + printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message); + } + } + catch (Exception e) + { + // do something sane in the face of an error + Console.WriteLine($"printer errored: {e}"); + } + } + + var debugConfig = SelfConfig.Instance?.Debug; + + if (debugConfig != null && debugConfig.HideMessagesForPerformance + && logQueue.Count > debugConfig.HideLogThreshold) + { // spam filtering (if queue has more than HideLogThreshold elements) + logWaitEvent.Reset(); // pause incoming log requests + + // clear loggers for this instance, to print the message to all affected logs + loggerLogger.printers.Clear(); + var prints = new HashSet(); + // clear the queue + while (logQueue.TryTake(out var message)) + { // aggregate loggers in the process + var messageLogger = message.Logger; + foreach (var print in messageLogger.printers) + prints.Add(print); + do + { + messageLogger = messageLogger.parent; + if (messageLogger != null) + foreach (var print in messageLogger.printers) + prints.Add(print); + } while (messageLogger != null); + } + + // print using logging subsystem to all logger printers + loggerLogger.printers.AddRange(prints); + logQueue.Add(new LogMessage + { // manually adding to the queue instead of using Warn() because calls to the logger are suspended here + Level = Level.Warning, + Logger = loggerLogger, + Message = $"{loggerLogger.logName.ToUpper()}: Messages omitted to improve performance", + Time = Utils.CurrentTime() + }); + + // resume log calls + logWaitEvent.Set(); + } + + var now = Utils.CurrentTime(); + var copy = new List(started); + foreach (var printer in copy) + { + // close printer after 500ms from its last use + if (now - printer.LastUse > timeout) + { + try + { + printer.EndPrint(); + } + catch (Exception e) + { + Console.WriteLine($"printer errored: {e}"); + } + + started.Remove(printer); + } + } + } + // wait for messages for 500ms before ending the prints + while (logQueue.TryTake(out msg, timeout)); + + if (logQueue.Count == 0) + { // when the queue has been empty for 500ms, end all prints + foreach (var printer in started) + { + try + { + printer.EndPrint(); + } + catch (Exception e) + { + Console.WriteLine($"printer errored: {e}"); + } + } + started.Clear(); + } + } + } + + /// + /// Stops and joins the log printer thread. + /// + internal static void StopLogThread() + { + logQueue.CompleteAdding(); + logThread.Join(); + } + } + + /// + /// A class providing extensions for various loggers. + /// + public static class LoggerExtensions + { + /// + /// Gets a child logger, if supported. Currently the only defined and supported logger is , and most plugins will only ever receive this anyway. + /// + /// the parent + /// the name of the child + /// the child logger + public static Logger GetChildLogger(this Logger logger, string name) + { + if (logger is StandardLogger standardLogger) + return standardLogger.GetChild(name); + + throw new InvalidOperationException(); + } + } } \ No newline at end of file