Browse Source

Some cleaning

4.0.0-beta
Anairkoen Schno 4 years ago
parent
commit
6c373b5dce
4 changed files with 717 additions and 721 deletions
  1. +1
    -1
      BuildTools
  2. +157
    -157
      IPA.Loader/Config/SelfConfig.cs
  3. +144
    -147
      IPA.Loader/Loader/PluginInitInjector.cs
  4. +415
    -416
      IPA.Loader/Logging/StandardLogger.cs

+ 1
- 1
BuildTools

@ -1 +1 @@
Subproject commit 1a4661172c07371760fd8b78ea7a39e5d17641d7
Subproject commit d1ee2f868935cf0407ba95dd617e618d297dfd86

+ 157
- 157
IPA.Loader/Config/SelfConfig.cs View File

@ -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<SelfConfig>();
}
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<string, HashSet<string>>))]
public virtual HashSet<string> GameAssemblies { get; set; } = new HashSet<string>
{
// 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<string> GameAssemblies_ => Instance?.GameAssemblies ?? new HashSet<string> { "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<SelfConfig>();
}
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<string, HashSet<string>>))]
public virtual HashSet<string> GameAssemblies { get; set; } = new HashSet<string>
{
// 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<string> GameAssemblies_ => Instance?.GameAssemblies ?? new HashSet<string> { "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;
}
}

+ 144
- 147
IPA.Loader/Loader/PluginInitInjector.cs View File

@ -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
{
/// <summary>
/// The type that handles value injecting into a plugin's Init.
/// </summary>
public static class PluginInitInjector
{
/// <summary>
/// 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.
/// </summary>
/// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param>
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param>
/// <param name="meta">the <see cref="PluginLoader.PluginMetadata"/> for the plugin being loaded.</param>
/// <returns>the value to inject into that parameter.</returns>
public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta);
/// <summary>
/// Adds an injector to be used when calling future plugins' Init methods.
/// </summary>
/// <param name="type">the type of the parameter.</param>
/// <param name="injector">the function to call for injection.</param>
public static void AddInjector(Type type, InjectParameter injector)
{
injectors.Add(new TypedInjector(type, injector));
}
private struct TypedInjector : IEquatable<TypedInjector>
{
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<TypedInjector> injectors = new List<TypedInjector>
{
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<object>();
var initParams = init.GetParameters();
var previousValues = new Dictionary<TypedInjector, object>(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
{
/// <summary>
/// The type that handles value injecting into a plugin's Init.
/// </summary>
public static class PluginInitInjector
{
/// <summary>
/// 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.
/// </summary>
/// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param>
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param>
/// <param name="meta">the <see cref="PluginLoader.PluginMetadata"/> for the plugin being loaded.</param>
/// <returns>the value to inject into that parameter.</returns>
public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta);
/// <summary>
/// Adds an injector to be used when calling future plugins' Init methods.
/// </summary>
/// <param name="type">the type of the parameter.</param>
/// <param name="injector">the function to call for injection.</param>
public static void AddInjector(Type type, InjectParameter injector)
{
injectors.Add(new TypedInjector(type, injector));
}
private struct TypedInjector : IEquatable<TypedInjector>
{
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<TypedInjector> injectors = new List<TypedInjector>
{
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<object>();
var initParams = init.GetParameters();
var previousValues = new Dictionary<TypedInjector, object>(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());
}
}
}

+ 415
- 416
IPA.Loader/Logging/StandardLogger.cs View File

@ -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
{
/// <summary>
/// The default (and standard) <see cref="Logger"/> implementation.
/// </summary>
/// <remarks>
/// <see cref="StandardLogger"/> 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 <see cref="LoggerExtensions.GetChildLogger"/> to safely get a child.
/// The modification of printers on a parent are reflected down the chain.
/// </remarks>
public class StandardLogger : Logger
{
private static readonly List<LogPrinter> defaultPrinters = new List<LogPrinter>()
{
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;
}
}
/// <summary>
/// The <see cref="TextWriter"/> for writing directly to the console window, or stdout if no window open.
/// </summary>
/// <value>a <see cref="TextWriter"/> for the current primary text output</value>
public static TextWriter ConsoleWriter { get; internal set; } = Console.Out;
/// <summary>
/// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger.
/// </summary>
/// <param name="printer">the printer to add</param>
internal static void AddDefaultPrinter(LogPrinter printer)
{
defaultPrinters.Add(printer);
}
private readonly string logName;
private static bool showSourceClass;
/// <summary>
/// All levels defined by this filter will be sent to loggers. All others will be ignored.
/// </summary>
/// <value>the global filter level</value>
public static LogLevel PrintFilter { get; set; } = LogLevel.All;
private static bool showTrace = false;
private readonly List<LogPrinter> printers = new List<LogPrinter>();
private readonly StandardLogger parent;
private readonly Dictionary<string, StandardLogger> children = new Dictionary<string, StandardLogger>();
/// <summary>
/// Configures internal debug settings based on the config passed in.
/// </summary>
/// <param name="cfg"></param>
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<LogPrinter>();
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();
}
}
/// <summary>
/// Gets a child printer with the given name, either constructing a new one or using one that was already made.
/// </summary>
/// <param name="name"></param>
/// <returns>a child <see cref="StandardLogger"/> with the given sub-name</returns>
internal StandardLogger GetChild(string name)
{
if (!children.TryGetValue(name, out var child))
{
child = new StandardLogger(this, name);
children.Add(name, child);
}
return child;
}
/// <summary>
/// Adds a log printer to the logger.
/// </summary>
/// <param name="printer">the printer to add</param>
public void AddPrinter(LogPrinter printer)
{
printers.Add(printer);
}
/// <summary>
/// Logs a specific message at a given level.
/// </summary>
/// <param name="level">the message level</param>
/// <param name="message">the message to log</param>
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()
});
}
/// <inheritdoc />
/// <summary>
/// An override to <see cref="M:IPA.Logging.Logger.Debug(System.String)" /> which shows the method that called it.
/// </summary>
/// <param name="message">the message to log</param>
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<LogMessage> logQueue = new BlockingCollection<LogMessage>();
private static Thread logThread;
private static StandardLogger loggerLogger;
private const int LogCloseTimeout = 250;
/// <summary>
/// The log printer thread for <see cref="StandardLogger"/>.
/// </summary>
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<LogPrinter>();
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<LogPrinter> 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<LogPrinter>();
// 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<LogPrinter>(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();
}
}
}
/// <summary>
/// Stops and joins the log printer thread.
/// </summary>
internal static void StopLogThread()
{
logQueue.CompleteAdding();
logThread.Join();
}
}
/// <summary>
/// A class providing extensions for various loggers.
/// </summary>
public static class LoggerExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <param name="logger">the parent <see cref="Logger"/></param>
/// <param name="name">the name of the child</param>
/// <returns>the child logger</returns>
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
{
/// <summary>
/// The default (and standard) <see cref="Logger"/> implementation.
/// </summary>
/// <remarks>
/// <see cref="StandardLogger"/> 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 <see cref="LoggerExtensions.GetChildLogger"/> to safely get a child.
/// The modification of printers on a parent are reflected down the chain.
/// </remarks>
public class StandardLogger : Logger
{
private static readonly List<LogPrinter> defaultPrinters = new List<LogPrinter>()
{
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;
}
}
/// <summary>
/// The <see cref="TextWriter"/> for writing directly to the console window, or stdout if no window open.
/// </summary>
/// <value>a <see cref="TextWriter"/> for the current primary text output</value>
public static TextWriter ConsoleWriter { get; internal set; } = Console.Out;
/// <summary>
/// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger.
/// </summary>
/// <param name="printer">the printer to add</param>
internal static void AddDefaultPrinter(LogPrinter printer)
{
defaultPrinters.Add(printer);
}
private readonly string logName;
private static bool showSourceClass;
/// <summary>
/// All levels defined by this filter will be sent to loggers. All others will be ignored.
/// </summary>
/// <value>the global filter level</value>
internal static LogLevel PrintFilter { get; set; } = LogLevel.All;
private static bool showTrace = false;
private readonly List<LogPrinter> printers = new List<LogPrinter>();
private readonly StandardLogger parent;
private readonly Dictionary<string, StandardLogger> children = new Dictionary<string, StandardLogger>();
/// <summary>
/// Configures internal debug settings based on the config passed in.
/// </summary>
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<LogPrinter>();
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();
}
}
/// <summary>
/// Gets a child printer with the given name, either constructing a new one or using one that was already made.
/// </summary>
/// <param name="name"></param>
/// <returns>a child <see cref="StandardLogger"/> with the given sub-name</returns>
internal StandardLogger GetChild(string name)
{
if (!children.TryGetValue(name, out var child))
{
child = new StandardLogger(this, name);
children.Add(name, child);
}
return child;
}
/// <summary>
/// Adds a log printer to the logger.
/// </summary>
/// <param name="printer">the printer to add</param>
public void AddPrinter(LogPrinter printer)
{
printers.Add(printer);
}
/// <summary>
/// Logs a specific message at a given level.
/// </summary>
/// <param name="level">the message level</param>
/// <param name="message">the message to log</param>
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()
});
}
/// <inheritdoc />
/// <summary>
/// An override to <see cref="M:IPA.Logging.Logger.Debug(System.String)" /> which shows the method that called it.
/// </summary>
/// <param name="message">the message to log</param>
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<LogMessage> logQueue = new BlockingCollection<LogMessage>();
private static Thread logThread;
private static StandardLogger loggerLogger;
private const int LogCloseTimeout = 250;
/// <summary>
/// The log printer thread for <see cref="StandardLogger"/>.
/// </summary>
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<LogPrinter>();
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<LogPrinter> 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<LogPrinter>();
// 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<LogPrinter>(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();
}
}
}
/// <summary>
/// Stops and joins the log printer thread.
/// </summary>
internal static void StopLogThread()
{
logQueue.CompleteAdding();
logThread.Join();
}
}
/// <summary>
/// A class providing extensions for various loggers.
/// </summary>
public static class LoggerExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <param name="logger">the parent <see cref="Logger"/></param>
/// <param name="name">the name of the child</param>
/// <returns>the child logger</returns>
public static Logger GetChildLogger(this Logger logger, string name)
{
if (logger is StandardLogger standardLogger)
return standardLogger.GetChild(name);
throw new InvalidOperationException();
}
}
}

Loading…
Cancel
Save