E5E7 v7: Redesign the plugin loading api to a phase model by aldelaro5 · Pull Request #999 · BepInEx/BepInEx · GitHub
[go: up one dir, main page]

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<Description>BepInEx plugin provider module</Description>
<IsPackable>false</IsPackable>
<RootNamespace>BepInEx.BootstrapPlugin</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand Down
3 changes: 0 additions & 3 deletions BepInEx.Core/BepInEx.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
<PackageReference Include="SemanticVersioning" Version="2.0.2"/>
<PackageReference Include="MonoMod.Utils" Version="22.7.31.1"/>
</ItemGroup>
<ItemGroup>
<Compile Remove="Contract\IPlugin.cs"/>
</ItemGroup>

<!-- CopyLocalLockFileAssemblies causes to also output shared assemblies: https://github.com/NuGet/Home/issues/4837#issuecomment-354536302 -->
<!-- Since all core assemblies usually follow naming of System.*, we just delete them for now -->
Expand Down
112 changes: 112 additions & 0 deletions BepInEx.Core/Bootstrap/Chainloader.cs
4B92
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BepInEx.Configuration;
using BepInEx.Core.Bootstrap;
using BepInEx.Logging;

namespace BepInEx;

/// <summary>
/// The base type of any chainloaders no matter the runtime
/// </summary>
public abstract class Chainloader
{
private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.Bind(
"Logging.Disk", "AppendLog",
false,
"Appends to the log file instead of overwriting, on game startup.");

private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.Bind(
"Logging.Disk", "Enabled",
true,
"Enables writing log messages to disk.");

private static readonly ConfigEntry<LogLevel> ConfigDiskLoggingDisplayedLevel = ConfigFile.CoreConfig.Bind(
"Logging.Disk", "LogLevels",
LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
"Only displays the specified log levels in the disk log output.");

private static readonly ConfigEntry<bool> ConfigDiskLoggingInstantFlushing = ConfigFile.CoreConfig.Bind(
"Logging.Disk", "InstantFlushing",
false,
new StringBuilder()
.AppendLine("If true, instantly writes any received log entries to disk.")
.AppendLine("This incurs a major performance hit if a lot of log messages are being written, however it is really useful for debugging crashes.")
.ToString());

private static readonly ConfigEntry<int> ConfigDiskLoggingFileLimit = ConfigFile.CoreConfig.Bind(
"Logging.Disk", "ConcurrentFileLimit",
5,
new StringBuilder()
.AppendLine("The maximum amount of concurrent log files that will be written to disk.")
.AppendLine("As one log file is used per open game instance, you may find it necessary to increase this limit when debugging multiple instances at the same time.")
.ToString());

internal Chainloader() { }

/// <summary>
/// The title of the console
/// </summary>
private string ConsoleTitle => $"BepInEx {Utility.BepInExVersion} - {Paths.ProcessName}";

/// <summary>
/// Whether the loading system was initialised
/// </summary>
private bool Initialized { get; set; }

/// <summary>
/// The current chainloader instance
/// </summary>
protected static Chainloader Instance { get; set; }

internal virtual void Initialize(string gameExePath = null)
{
if (Initialized)
throw new InvalidOperationException("Chainloader cannot be initialized multiple times");

Instance = this;

// Set vitals
if (gameExePath != null)
// Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize
// This is used by Preloader to use environment variables, for example
Paths.SetExecutablePath(gameExePath);

InitializeLoggers();

if (!Directory.Exists(Paths.PluginPath))
Directory.CreateDirectory(Paths.PluginPath);

Initialized = true;

Logger.Log(LogLevel.Message, "Chainloader initialized");
}

internal virtual void InitializeLoggers()
{
if (ConsoleManager.ConsoleEnabled && !ConsoleManager.ConsoleActive)
ConsoleManager.CreateConsole();

if (ConsoleManager.ConsoleActive)
{
if (!Logger.Listeners.Any(x => x is ConsoleLogListener))
Logger.Listeners.Add(new ConsoleLogListener());

ConsoleManager.SetConsoleTitle(ConsoleTitle);
}

if (ConfigDiskLogging.Value)
Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskLoggingDisplayedLevel.Value,
ConfigDiskAppend.Value, ConfigDiskLoggingInstantFlushing.Value,
ConfigDiskLoggingFileLimit.Value));

if (!TraceLogSource.IsListening)
Logger.Sources.Add(TraceLogSource.CreateSource());

if (!Logger.Sources.Any(x => x is HarmonyLogSource))
Logger.Sources.Add(new HarmonyLogSource());
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.IO;

namespace BepInEx.PluginProvider;
namespace BepInEx.Core.Bootstrap;

internal class BepInExPluginLoadContext : IPluginLoadContext
internal class DefaultPluginLoadContext : IPluginLoadContext
{
public string AssemblyIdentifier { get; internal set; }
public string AssemblyHash { get; internal set; }
Expand All @@ -26,7 +26,7 @@ public byte[] GetFile(string relativePath)
throw new ArgumentNullException(nameof(relativePath));

string assemblyFolder = Path.GetDirectoryName(AssemblyIdentifier);
string filePath = Path.Combine(assemblyFolder, relativePath);
string filePath = assemblyFolder is null ? relativePath : Path.Combine(assemblyFolder, relativePath);
return File.ReadAllBytes(filePath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
using System.Reflection;
using BepInEx.Logging;

namespace BepInEx.PluginProvider;
namespace BepInEx.Core.Bootstrap;

[BepInPluginProvider("BepInExPluginProvider", "BepInExPluginProvider", "1.0")]
internal class BepInExPluginProvider : BasePluginProvider
internal class DefaultPluginProvider
{
private static readonly Dictionary<string, string> AssemblyLocationsByFilename = new();

public override IList<IPluginLoadContext> GetPlugins()
internal void Initialize()
{
Logger.Log(LogLevel.Message, "Started Initialise of default provider");
AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ResolveAssembly(args.Name);
PhaseManager.Instance.OnPhaseStarted += phase =>
{
Logger.Log(LogLevel.Message, $"Providing on phase {phase}");
PluginManager.Instance.Providers.Add(new(), GetLoadContexts);
};
Logger.Log(LogLevel.Message, "Ended Initialise of default provider");
}

private IList<IPluginLoadContext> GetLoadContexts()
{
var loadContexts = new List<IPluginLoadContext>();
foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PluginPath), "*.dll", SearchOption.AllDirectories))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about caching all of these contexts (as cached contexts) instead of reiterating over the files on every phase? Or do we expect dll files to be dropped at runtime? Wouldn't this add up across different providers having to reload on every phase?

D73A

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a discussion item in the OP because it depends if we expect the discovery to not change at entrypoint time

Expand All @@ -22,26 +33,27 @@ public override IList<IPluginLoadContext> GetPlugins()
var filename = Path.GetFileNameWithoutExtension(dll);
var foundDirectory = Path.GetDirectoryName(dll);

// Prioritize the shallowest path of each assembly name
if (AssemblyLocationsByFilename.TryGetValue(filename, out var existingDirectory))
// Prioritize the shallowest path of each assembly name, but only resolve the conflict once on the first phase
if (PhaseManager.Instance.CurrentPhase == BepInPhases.Entrypoint
&& AssemblyLocationsByFilename.TryGetValue(filename, out var existingDirectory))
{
int levelExistingDirectory = existingDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0;
int levelFoundDirectory = foundDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0;

bool shallowerPathFound = levelExistingDirectory > levelFoundDirectory;
Logger.LogWarning($"Found duplicate assemblies filenames: {filename} was found at {foundDirectory} " +
$"while it exists already at {AssemblyLocationsByFilename[filename]}. " +
$"Only the {(shallowerPathFound ? "first" : "second")} will be examined and resolved");
Logger.Log(LogLevel.Warning, $"Found duplicate assemblies filenames: {filename} was found at {foundDirectory} " +
$"while it exists already at {AssemblyLocationsByFilename[filename]}. " +
$"Only the {(shallowerPathFound ? "first" : "second")} will be examined and resolved");

if (levelExistingDirectory > levelFoundDirectory)
AssemblyLocationsByFilename[filename] = foundDirectory;
}
else
{
AssemblyLocationsByFilename.Add(filename, foundDirectory);
AssemblyLocationsByFilename[filename] = foundDirectory;
}

loadContexts.Add(new BepInExPluginLoadContext
loadContexts.Add(new DefaultPluginLoadContext
{
AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"),
AssemblyIdentifier = dll
Expand All @@ -56,7 +68,7 @@ public override IList<IPluginLoadContext> GetPlugins()
return loadContexts;
}

public override Assembly ResolveAssembly(string name)
private Assembly ResolveAssembly(string name)
{
if (!AssemblyLocationsByFilename.TryGetValue(name, out var location))
return null;
Expand Down
40 changes: 40 additions & 0 deletions BepInEx.Core/Bootstrap/PhaseManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using BepInEx.Logging;

namespace BepInEx.Core.Bootstrap;

/// <summary>
/// The manager class that allows to start phases and to listen when a phase starts
/// </summary>
public class PhaseManager
{
/// <summary>
/// The current instance of the phase manager
/// </summary>
public static PhaseManager Instance { get; } = new();

private PhaseManager() { }

/// <summary>
/// The current phase
/// </summary>
public string CurrentPhase { get; private set; }

/// <summary>
/// Occurs when a phase starts
/// </summary>
public event Action<string> OnPhaseStarted;

/// <summary>
/// Starts a phase
/// </summary>
/// <param name="phase">The name of the phase</param>
/// <seealso cref="BepInPhases"/>
public void StartPhase(string phase)
{
Logger.Log(LogLevel.Message, "Started phase " + phase);
CurrentPhase = phase;
OnPhaseStarted?.Invoke(phase);
Logger.Log(LogLevel.Message, "Ended phase " + phase);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging overall seems pretty excessive or at least at wrong levels. Something to look into later once things are getting finalized.

This seems like a good place to add a StopWatch and log how long a phase took.

}
}
32 changes: 32 additions & 0 deletions BepInEx.Core/Bootstrap/PluginLoadEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Reflection;

namespace BepInEx.Core.Bootstrap;

/// <summary>
/// Info about the loading of a plugin
/// </summary>
public class PluginLoadEventArgs : EventArgs
{
/// <summary>
/// The concerned plugin
/// </summary>
public PluginInfo PluginInfo { get; internal set; }

/// <summary>
/// The plugin's assembly
/// </summary>
public Assembly Assembly { get; internal set; }

/// <summary>
/// The plugin's instance
/// </summary>
public Plugin PluginInstance { get; internal set; }

internal PluginLoadEventArgs(PluginInfo pluginInfo, Assembly assembly, Plugin pluginInstance)
{
PluginInfo = pluginInfo;
Assembly = assembly;
PluginInstance = pluginInstance;
}
}
3486
Loading
0