Merge branch 'master' into ParentRepo

This commit is contained in:
goat 2022-11-03 21:50:23 +01:00 committed by GitHub
commit 24e35255c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1628 changed files with 270076 additions and 121885 deletions

View file

@ -1,22 +1,21 @@
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This represents a banned plugin that attempted an operation.
/// </summary>
internal class BannedPluginException : PluginException
{
/// <summary>
/// This represents a banned plugin that attempted an operation.
/// Initializes a new instance of the <see cref="BannedPluginException"/> class.
/// </summary>
internal class BannedPluginException : PluginException
/// <param name="message">The message describing the invalid operation.</param>
public BannedPluginException(string message)
{
/// <summary>
/// Initializes a new instance of the <see cref="BannedPluginException"/> class.
/// </summary>
/// <param name="message">The message describing the invalid operation.</param>
public BannedPluginException(string message)
{
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
}

View file

@ -1,26 +1,25 @@
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This exception that is thrown when a plugin is instructed to load while another plugin with the same
/// assembly name is already present and loaded.
/// </summary>
internal class DuplicatePluginException : PluginException
{
/// <summary>
/// This exception that is thrown when a plugin is instructed to load while another plugin with the same
/// assembly name is already present and loaded.
/// Initializes a new instance of the <see cref="DuplicatePluginException"/> class.
/// </summary>
internal class DuplicatePluginException : PluginException
/// <param name="assemblyName">Name of the conflicting assembly.</param>
public DuplicatePluginException(string assemblyName)
{
/// <summary>
/// Initializes a new instance of the <see cref="DuplicatePluginException"/> class.
/// </summary>
/// <param name="assemblyName">Name of the conflicting assembly.</param>
public DuplicatePluginException(string assemblyName)
{
this.AssemblyName = assemblyName;
}
/// <summary>
/// Gets the name of the conflicting assembly.
/// </summary>
public string AssemblyName { get; init; }
/// <inheritdoc/>
public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded";
this.AssemblyName = assemblyName;
}
/// <summary>
/// Gets the name of the conflicting assembly.
/// </summary>
public string AssemblyName { get; init; }
/// <inheritdoc/>
public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded";
}

View file

@ -1,24 +1,23 @@
using System.IO;
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This exception represents a file that does not implement IDalamudPlugin.
/// </summary>
internal class InvalidPluginException : PluginException
{
/// <summary>
/// This exception represents a file that does not implement IDalamudPlugin.
/// Initializes a new instance of the <see cref="InvalidPluginException"/> class.
/// </summary>
internal class InvalidPluginException : PluginException
/// <param name="dllFile">The invalid file.</param>
public InvalidPluginException(FileInfo dllFile)
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidPluginException"/> class.
/// </summary>
/// <param name="dllFile">The invalid file.</param>
public InvalidPluginException(FileInfo dllFile)
{
this.DllFile = dllFile;
}
/// <summary>
/// Gets the invalid file.
/// </summary>
public FileInfo DllFile { get; init; }
this.DllFile = dllFile;
}
/// <summary>
/// Gets the invalid file.
/// </summary>
public FileInfo DllFile { get; init; }
}

View file

@ -1,22 +1,21 @@
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This represents an invalid plugin operation.
/// </summary>
internal class InvalidPluginOperationException : PluginException
{
/// <summary>
/// This represents an invalid plugin operation.
/// Initializes a new instance of the <see cref="InvalidPluginOperationException"/> class.
/// </summary>
internal class InvalidPluginOperationException : PluginException
/// <param name="message">The message describing the invalid operation.</param>
public InvalidPluginOperationException(string message)
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidPluginOperationException"/> class.
/// </summary>
/// <param name="message">The message describing the invalid operation.</param>
public InvalidPluginOperationException(string message)
{
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
}

View file

@ -1,11 +1,10 @@
using System;
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This represents the base Dalamud plugin exception.
/// </summary>
internal abstract class PluginException : Exception
{
/// <summary>
/// This represents the base Dalamud plugin exception.
/// </summary>
internal abstract class PluginException : Exception
{
}
}

View file

@ -9,308 +9,307 @@ using System.Runtime.Loader;
using Dalamud.Plugin.Internal.Loader.LibraryModel;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
/// </summary>
internal class AssemblyLoadContextBuilder
{
private readonly List<string> additionalProbingPaths = new();
private readonly List<string> resourceProbingPaths = new();
private readonly List<string> resourceProbingSubpaths = new();
private readonly Dictionary<string, ManagedLibrary> managedLibraries = new(StringComparer.Ordinal);
private readonly Dictionary<string, NativeLibrary> nativeLibraries = new(StringComparer.Ordinal);
private readonly HashSet<string> privateAssemblies = new(StringComparer.Ordinal);
private readonly HashSet<string> defaultAssemblies = new(StringComparer.Ordinal);
private AssemblyLoadContext defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
private string? mainAssemblyPath;
private bool preferDefaultLoadContext;
private bool isCollectible;
private bool loadInMemory;
private bool shadowCopyNativeLibraries;
/// <summary>
/// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
/// Creates an assembly load context using settings specified on the builder.
/// </summary>
internal class AssemblyLoadContextBuilder
/// <returns>A new ManagedLoadContext.</returns>
public AssemblyLoadContext Build()
{
private readonly List<string> additionalProbingPaths = new();
private readonly List<string> resourceProbingPaths = new();
private readonly List<string> resourceProbingSubpaths = new();
private readonly Dictionary<string, ManagedLibrary> managedLibraries = new(StringComparer.Ordinal);
private readonly Dictionary<string, NativeLibrary> nativeLibraries = new(StringComparer.Ordinal);
private readonly HashSet<string> privateAssemblies = new(StringComparer.Ordinal);
private readonly HashSet<string> defaultAssemblies = new(StringComparer.Ordinal);
private AssemblyLoadContext defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
private string? mainAssemblyPath;
private bool preferDefaultLoadContext;
private bool isCollectible;
private bool loadInMemory;
private bool shadowCopyNativeLibraries;
/// <summary>
/// Creates an assembly load context using settings specified on the builder.
/// </summary>
/// <returns>A new ManagedLoadContext.</returns>
public AssemblyLoadContext Build()
var resourceProbingPaths = new List<string>(this.resourceProbingPaths);
foreach (var additionalPath in this.additionalProbingPaths)
{
var resourceProbingPaths = new List<string>(this.resourceProbingPaths);
foreach (var additionalPath in this.additionalProbingPaths)
foreach (var subPath in this.resourceProbingSubpaths)
{
foreach (var subPath in this.resourceProbingSubpaths)
{
resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
}
resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
}
}
if (this.mainAssemblyPath == null)
throw new InvalidOperationException($"Missing required property. You must call '{nameof(this.SetMainAssemblyPath)}' to configure the default assembly.");
return new ManagedLoadContext(
this.mainAssemblyPath,
this.managedLibraries,
this.nativeLibraries,
this.privateAssemblies,
this.defaultAssemblies,
this.additionalProbingPaths,
resourceProbingPaths,
this.defaultLoadContext,
this.preferDefaultLoadContext,
this.isCollectible,
this.loadInMemory,
this.shadowCopyNativeLibraries);
}
/// <summary>
/// Set the file path to the main assembly for the context. This is used as the starting point for loading
/// other assemblies. The directory that contains it is also known as the 'app local' directory.
/// </summary>
/// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Argument must not be null or empty.", nameof(path));
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
this.mainAssemblyPath = path;
return this;
}
/// <summary>
/// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
/// <param name="context">The context to set.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context)
{
this.defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(this.SetDefaultContext)} is null.");
return this;
}
/// <summary>
/// Instructs the load context to prefer a private version of this assembly, even if that version is
/// different from the version used by the host application.
/// Use this when you do not need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from
/// this assembly will not match the types from an assembly with the same name, but different version,
/// in the host application.
/// </para>
/// <para>
/// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c>
/// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects,
/// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same
/// type. <c>Foo1.GetType() != Foo2.GetType()</c>.
/// </para>
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName)
{
if (assemblyName.Name != null)
this.privateAssemblies.Add(assemblyName.Name);
return this;
}
/// <summary>
/// Instructs the load context to first attempt to load assemblies by this name from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
{
var names = new Queue<AssemblyName>(new[] { assemblyName });
while (names.TryDequeue(out var name))
{
if (name.Name == null || this.defaultAssemblies.Contains(name.Name))
{
// base cases
continue;
}
if (this.mainAssemblyPath == null)
throw new InvalidOperationException($"Missing required property. You must call '{nameof(this.SetMainAssemblyPath)}' to configure the default assembly.");
this.defaultAssemblies.Add(name.Name);
return new ManagedLoadContext(
this.mainAssemblyPath,
this.managedLibraries,
this.nativeLibraries,
this.privateAssemblies,
this.defaultAssemblies,
this.additionalProbingPaths,
resourceProbingPaths,
this.defaultLoadContext,
this.preferDefaultLoadContext,
this.isCollectible,
this.loadInMemory,
this.shadowCopyNativeLibraries);
}
// Load and find all dependencies of default assemblies.
// This sacrifices some performance for determinism in how transitive
// dependencies will be shared between host and plugin.
var assembly = this.defaultLoadContext.LoadFromAssemblyName(name);
/// <summary>
/// Set the file path to the main assembly for the context. This is used as the starting point for loading
/// other assemblies. The directory that contains it is also known as the 'app local' directory.
/// </summary>
/// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Argument must not be null or empty.", nameof(path));
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
this.mainAssemblyPath = path;
return this;
}
/// <summary>
/// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
/// <param name="context">The context to set.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context)
{
this.defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(this.SetDefaultContext)} is null.");
return this;
}
/// <summary>
/// Instructs the load context to prefer a private version of this assembly, even if that version is
/// different from the version used by the host application.
/// Use this when you do not need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from
/// this assembly will not match the types from an assembly with the same name, but different version,
/// in the host application.
/// </para>
/// <para>
/// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c>
/// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects,
/// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same
/// type. <c>Foo1.GetType() != Foo2.GetType()</c>.
/// </para>
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName)
{
if (assemblyName.Name != null)
this.privateAssemblies.Add(assemblyName.Name);
return this;
}
/// <summary>
/// Instructs the load context to first attempt to load assemblies by this name from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
{
var names = new Queue<AssemblyName>(new[] { assemblyName });
while (names.TryDequeue(out var name))
foreach (var reference in assembly.GetReferencedAssemblies())
{
if (name.Name == null || this.defaultAssemblies.Contains(name.Name))
{
// base cases
continue;
}
this.defaultAssemblies.Add(name.Name);
// Load and find all dependencies of default assemblies.
// This sacrifices some performance for determinism in how transitive
// dependencies will be shared between host and plugin.
var assembly = this.defaultLoadContext.LoadFromAssemblyName(name);
foreach (var reference in assembly.GetReferencedAssemblies())
{
names.Enqueue(reference);
}
names.Enqueue(reference);
}
return this;
}
/// <summary>
/// Instructs the load context to first search for binaries from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from within the context are force-downgraded to the version provided
/// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries
/// which should not be loaded from the default load context.
/// </para>
/// </summary>
/// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext)
return this;
}
/// <summary>
/// Instructs the load context to first search for binaries from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from within the context are force-downgraded to the version provided
/// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries
/// which should not be loaded from the default load context.
/// </para>
/// </summary>
/// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext)
{
this.preferDefaultLoadContext = preferDefaultLoadContext;
return this;
}
/// <summary>
/// Add a managed library to the load context.
/// </summary>
/// <param name="library">The managed library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library)
{
ValidateRelativePath(library.AdditionalProbingPath);
if (library.Name.Name != null)
{
this.preferDefaultLoadContext = preferDefaultLoadContext;
return this;
this.managedLibraries.Add(library.Name.Name, library);
}
/// <summary>
/// Add a managed library to the load context.
/// </summary>
/// <param name="library">The managed library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library)
{
ValidateRelativePath(library.AdditionalProbingPath);
return this;
}
if (library.Name.Name != null)
{
this.managedLibraries.Add(library.Name.Name, library);
}
/// <summary>
/// Add a native library to the load context.
/// </summary>
/// <param name="library">A native library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
{
ValidateRelativePath(library.AppLocalPath);
ValidateRelativePath(library.AdditionalProbingPath);
return this;
}
this.nativeLibraries.Add(library.Name, library);
/// <summary>
/// Add a native library to the load context.
/// </summary>
/// <param name="library">A native library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
{
ValidateRelativePath(library.AppLocalPath);
ValidateRelativePath(library.AdditionalProbingPath);
return this;
}
this.nativeLibraries.Add(library.Name, library);
/// <summary>
/// Add a <paramref name="path"/> that should be used to search for native and managed libraries.
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
return this;
}
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
/// <summary>
/// Add a <paramref name="path"/> that should be used to search for native and managed libraries.
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
this.additionalProbingPaths.Add(path);
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
return this;
}
this.additionalProbingPaths.Add(path);
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies).
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddResourceProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
return this;
}
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies).
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddResourceProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
this.resourceProbingPaths.Add(path);
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
return this;
}
this.resourceProbingPaths.Add(path);
/// <summary>
/// Enable unloading the assembly load context.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder EnableUnloading()
{
this.isCollectible = true;
return this;
}
return this;
}
/// <summary>
/// Enable unloading the assembly load context.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder EnableUnloading()
{
this.isCollectible = true;
/// <summary>
/// Read .dll files into memory to avoid locking the files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory()
{
this.loadInMemory = true;
return this;
}
return this;
}
/// <summary>
/// Read .dll files into memory to avoid locking the files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory()
{
this.loadInMemory = true;
/// <summary>
/// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading of plugins dependent on native libraries.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder ShadowCopyNativeLibraries()
{
this.shadowCopyNativeLibraries = true;
return this;
}
return this;
}
/// <summary>
/// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading of plugins dependent on native libraries.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder ShadowCopyNativeLibraries()
{
this.shadowCopyNativeLibraries = true;
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies)
/// relative to any paths specified as <see cref="AddProbingPath"/>.
/// </summary>
/// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param>
/// <returns>The builder.</returns>
internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
return this;
}
if (Path.IsPathRooted(path))
throw new ArgumentException("Argument must be not a full path.", nameof(path));
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies)
/// relative to any paths specified as <see cref="AddProbingPath"/>.
/// </summary>
/// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param>
/// <returns>The builder.</returns>
internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
this.resourceProbingSubpaths.Add(path);
if (Path.IsPathRooted(path))
throw new ArgumentException("Argument must be not a full path.", nameof(path));
return this;
}
this.resourceProbingSubpaths.Add(path);
private static void ValidateRelativePath(string probingPath)
{
if (string.IsNullOrEmpty(probingPath))
throw new ArgumentException("Value must not be null or empty.", nameof(probingPath));
return this;
}
private static void ValidateRelativePath(string probingPath)
{
if (string.IsNullOrEmpty(probingPath))
throw new ArgumentException("Value must not be null or empty.", nameof(probingPath));
if (Path.IsPathRooted(probingPath))
throw new ArgumentException("Argument must be a relative path.", nameof(probingPath));
}
if (Path.IsPathRooted(probingPath))
throw new ArgumentException("Argument must be a relative path.", nameof(probingPath));
}
}

View file

@ -6,67 +6,66 @@ using System.Diagnostics;
using System.IO;
using System.Reflection;
namespace Dalamud.Plugin.Internal.Loader.LibraryModel
namespace Dalamud.Plugin.Internal.Loader.LibraryModel;
/// <summary>
/// Represents a managed, .NET assembly.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class ManagedLibrary
{
/// <summary>
/// Represents a managed, .NET assembly.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class ManagedLibrary
private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
{
private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
}
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
}
/// <summary>
/// Gets the name of the managed library.
/// </summary>
public AssemblyName Name { get; }
/// <summary>
/// Gets the name of the managed library.
/// </summary>
public AssemblyName Name { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For most managed libraries, this will be the file name.
/// For example, <c>MyPlugin1.dll</c>.
/// </para>
/// <para>
/// For runtime-specific managed implementations, this may include a sub folder path.
/// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For most managed libraries, this will be the file name.
/// For example, <c>MyPlugin1.dll</c>.
/// </para>
/// <para>
/// For runtime-specific managed implementations, this may include a sub folder path.
/// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A managed library.</returns>
public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
// When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
// The SDK will not flatten managed libraries found under runtimes/
var appLocalPath = assetPath.StartsWith("lib/")
? Path.GetFileName(assetPath)
: assetPath;
/// <summary>
/// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A managed library.</returns>
public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
// When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
// The SDK will not flatten managed libraries found under runtimes/
var appLocalPath = assetPath.StartsWith("lib/")
? Path.GetFileName(assetPath)
: assetPath;
return new ManagedLibrary(
new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
appLocalPath);
}
return new ManagedLibrary(
new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
appLocalPath);
}
}

View file

@ -5,64 +5,63 @@ using System;
using System.Diagnostics;
using System.IO;
namespace Dalamud.Plugin.Internal.Loader.LibraryModel
namespace Dalamud.Plugin.Internal.Loader.LibraryModel;
/// <summary>
/// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
/// for P/Invoke to work.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class NativeLibrary
{
/// <summary>
/// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
/// for P/Invoke to work.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class NativeLibrary
private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
{
private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
}
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
}
/// <summary>
/// Gets the name of the native library. This should match the name of the P/Invoke call.
/// <para>
/// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>.
/// This may not match the exact file name as loading will attempt variations on the name according
/// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
/// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
/// `sqlite3.so` and `libsqlite3.so`.
/// </para>
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the name of the native library. This should match the name of the P/Invoke call.
/// <para>
/// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>.
/// This may not match the exact file name as loading will attempt variations on the name according
/// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
/// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
/// `sqlite3.so` and `libsqlite3.so`.
/// </para>
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For example, <c>runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For example, <c>runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Create an instance of <see cref="NativeLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A native library.</returns>
public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
return new NativeLibrary(
Path.GetFileNameWithoutExtension(assetPath),
assetPath,
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath));
}
/// <summary>
/// Create an instance of <see cref="NativeLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A native library.</returns>
public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
return new NativeLibrary(
Path.GetFileNameWithoutExtension(assetPath),
assetPath,
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath));
}
}

View file

@ -7,71 +7,70 @@ using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// Represents the configuration for a plugin loader.
/// </summary>
internal class LoaderConfig
{
/// <summary>
/// Represents the configuration for a plugin loader.
/// Initializes a new instance of the <see cref="LoaderConfig"/> class.
/// </summary>
internal class LoaderConfig
/// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
public LoaderConfig(string mainAssemblyPath)
{
/// <summary>
/// Initializes a new instance of the <see cref="LoaderConfig"/> class.
/// </summary>
/// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
public LoaderConfig(string mainAssemblyPath)
{
if (string.IsNullOrEmpty(mainAssemblyPath))
throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
if (string.IsNullOrEmpty(mainAssemblyPath))
throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
if (!Path.IsPathRooted(mainAssemblyPath))
throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
if (!Path.IsPathRooted(mainAssemblyPath))
throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
if (!File.Exists(mainAssemblyPath))
throw new ArgumentException("Value must exist", nameof(mainAssemblyPath));
if (!File.Exists(mainAssemblyPath))
throw new ArgumentException("Value must exist", nameof(mainAssemblyPath));
this.MainAssemblyPath = mainAssemblyPath;
}
/// <summary>
/// Gets the file path to the main assembly.
/// </summary>
public string MainAssemblyPath { get; }
/// <summary>
/// Gets a list of assemblies which should be treated as private.
/// </summary>
public ICollection<AssemblyName> PrivateAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets a list of assemblies which should be unified between the host and the plugin.
/// </summary>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
public ICollection<AssemblyName> SharedAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host.
/// <para>
/// This does not guarantee types will unify.
/// </para>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
/// </summary>
public bool PreferSharedTypes { get; set; }
/// <summary>
/// Gets or sets the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
/// <summary>
/// Gets or sets a value indicating whether the plugin can be unloaded from memory.
/// </summary>
public bool IsUnloadable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to load assemblies into memory in order to not lock files.
/// </summary>
public bool LoadInMemory { get; set; }
this.MainAssemblyPath = mainAssemblyPath;
}
/// <summary>
/// Gets the file path to the main assembly.
/// </summary>
public string MainAssemblyPath { get; }
/// <summary>
/// Gets a list of assemblies which should be treated as private.
/// </summary>
public ICollection<AssemblyName> PrivateAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets a list of assemblies which should be unified between the host and the plugin.
/// </summary>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
public ICollection<AssemblyName> SharedAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host.
/// <para>
/// This does not guarantee types will unify.
/// </para>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
/// </summary>
public bool PreferSharedTypes { get; set; }
/// <summary>
/// Gets or sets the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
/// <summary>
/// Gets or sets a value indicating whether the plugin can be unloaded from memory.
/// </summary>
public bool IsUnloadable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to load assemblies into memory in order to not lock files.
/// </summary>
public bool LoadInMemory { get; set; }
}

View file

@ -11,387 +11,386 @@ using System.Runtime.Loader;
using Dalamud.Plugin.Internal.Loader.LibraryModel;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native
/// binaries at runtime immitating some of the behaviors of corehost.
/// </summary>
[DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")]
internal class ManagedLoadContext : AssemblyLoadContext
{
private readonly string basePath;
private readonly string mainAssemblyPath;
private readonly IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies;
private readonly IReadOnlyDictionary<string, NativeLibrary> nativeLibraries;
private readonly IReadOnlyCollection<string> privateAssemblies;
private readonly ICollection<string> defaultAssemblies;
private readonly IReadOnlyCollection<string> additionalProbingPaths;
private readonly bool preferDefaultLoadContext;
private readonly string[] resourceRoots;
private readonly bool loadInMemory;
private readonly AssemblyLoadContext defaultLoadContext;
private readonly AssemblyDependencyResolver dependencyResolver;
private readonly bool shadowCopyNativeLibraries;
private readonly string unmanagedDllShadowCopyDirectoryPath;
/// <summary>
/// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native
/// binaries at runtime immitating some of the behaviors of corehost.
/// Initializes a new instance of the <see cref="ManagedLoadContext"/> class.
/// </summary>
[DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")]
internal class ManagedLoadContext : AssemblyLoadContext
/// <param name="mainAssemblyPath">Main assembly path.</param>
/// <param name="managedAssemblies">Managed assemblies.</param>
/// <param name="nativeLibraries">Native assemblies.</param>
/// <param name="privateAssemblies">Private assemblies.</param>
/// <param name="defaultAssemblies">Default assemblies.</param>
/// <param name="additionalProbingPaths">Additional probing paths.</param>
/// <param name="resourceProbingPaths">Resource probing paths.</param>
/// <param name="defaultLoadContext">Default load context.</param>
/// <param name="preferDefaultLoadContext">If the default load context should be prefered.</param>
/// <param name="isCollectible">If the dll is collectible.</param>
/// <param name="loadInMemory">If the dll should be loaded in memory.</param>
/// <param name="shadowCopyNativeLibraries">If native libraries should be shadow copied.</param>
public ManagedLoadContext(
string mainAssemblyPath,
IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
IReadOnlyCollection<string> privateAssemblies,
IReadOnlyCollection<string> defaultAssemblies,
IReadOnlyCollection<string> additionalProbingPaths,
IReadOnlyCollection<string> resourceProbingPaths,
AssemblyLoadContext defaultLoadContext,
bool preferDefaultLoadContext,
bool isCollectible,
bool loadInMemory,
bool shadowCopyNativeLibraries)
: base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible)
{
private readonly string basePath;
private readonly string mainAssemblyPath;
private readonly IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies;
private readonly IReadOnlyDictionary<string, NativeLibrary> nativeLibraries;
private readonly IReadOnlyCollection<string> privateAssemblies;
private readonly ICollection<string> defaultAssemblies;
private readonly IReadOnlyCollection<string> additionalProbingPaths;
private readonly bool preferDefaultLoadContext;
private readonly string[] resourceRoots;
private readonly bool loadInMemory;
private readonly AssemblyLoadContext defaultLoadContext;
private readonly AssemblyDependencyResolver dependencyResolver;
private readonly bool shadowCopyNativeLibraries;
private readonly string unmanagedDllShadowCopyDirectoryPath;
if (resourceProbingPaths == null)
throw new ArgumentNullException(nameof(resourceProbingPaths));
/// <summary>
/// Initializes a new instance of the <see cref="ManagedLoadContext"/> class.
/// </summary>
/// <param name="mainAssemblyPath">Main assembly path.</param>
/// <param name="managedAssemblies">Managed assemblies.</param>
/// <param name="nativeLibraries">Native assemblies.</param>
/// <param name="privateAssemblies">Private assemblies.</param>
/// <param name="defaultAssemblies">Default assemblies.</param>
/// <param name="additionalProbingPaths">Additional probing paths.</param>
/// <param name="resourceProbingPaths">Resource probing paths.</param>
/// <param name="defaultLoadContext">Default load context.</param>
/// <param name="preferDefaultLoadContext">If the default load context should be prefered.</param>
/// <param name="isCollectible">If the dll is collectible.</param>
/// <param name="loadInMemory">If the dll should be loaded in memory.</param>
/// <param name="shadowCopyNativeLibraries">If native libraries should be shadow copied.</param>
public ManagedLoadContext(
string mainAssemblyPath,
IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
IReadOnlyCollection<string> privateAssemblies,
IReadOnlyCollection<string> defaultAssemblies,
IReadOnlyCollection<string> additionalProbingPaths,
IReadOnlyCollection<string> resourceProbingPaths,
AssemblyLoadContext defaultLoadContext,
bool preferDefaultLoadContext,
bool isCollectible,
bool loadInMemory,
bool shadowCopyNativeLibraries)
: base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible)
this.mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
this.dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
this.basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException("Invalid assembly path", nameof(mainAssemblyPath));
this.managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
this.privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
this.defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
this.nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
this.additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
this.defaultLoadContext = defaultLoadContext;
this.preferDefaultLoadContext = preferDefaultLoadContext;
this.loadInMemory = loadInMemory;
this.resourceRoots = new[] { this.basePath }
.Concat(resourceProbingPaths)
.ToArray();
this.shadowCopyNativeLibraries = shadowCopyNativeLibraries;
this.unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
if (shadowCopyNativeLibraries)
{
if (resourceProbingPaths == null)
throw new ArgumentNullException(nameof(resourceProbingPaths));
this.Unloading += _ => this.OnUnloaded();
}
}
this.mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
this.dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
this.basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException("Invalid assembly path", nameof(mainAssemblyPath));
this.managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
this.privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
this.defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
this.nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
this.additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
this.defaultLoadContext = defaultLoadContext;
this.preferDefaultLoadContext = preferDefaultLoadContext;
this.loadInMemory = loadInMemory;
/// <summary>
/// Load an assembly from a filepath.
/// </summary>
/// <param name="path">Assembly path.</param>
/// <returns>A loaded assembly.</returns>
public Assembly LoadAssemblyFromFilePath(string path)
{
if (!this.loadInMemory)
return this.LoadFromAssemblyPath(path);
this.resourceRoots = new[] { this.basePath }
.Concat(resourceProbingPaths)
.ToArray();
using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
this.shadowCopyNativeLibraries = shadowCopyNativeLibraries;
this.unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var pdbPath = Path.ChangeExtension(path, ".pdb");
if (File.Exists(pdbPath))
{
using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return this.LoadFromStream(file, pdbFile);
}
if (shadowCopyNativeLibraries)
return this.LoadFromStream(file);
}
/// <summary>
/// Load an assembly.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Loaded assembly.</returns>
protected override Assembly? Load(AssemblyName assemblyName)
{
if (assemblyName.Name == null)
{
// not sure how to handle this case. It's technically possible.
return null;
}
if ((this.preferDefaultLoadContext || this.defaultAssemblies.Contains(assemblyName.Name)) && !this.privateAssemblies.Contains(assemblyName.Name))
{
// If default context is preferred, check first for types in the default context unless the dependency has been declared as private
try
{
this.Unloading += _ => this.OnUnloaded();
var defaultAssembly = this.defaultLoadContext.LoadFromAssemblyName(assemblyName);
if (defaultAssembly != null)
{
// Older versions used to return null here such that returned assembly would be resolved from the default ALC.
// However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
// this context was built. As such, we simply return the Assembly from the user's chosen default load context.
return defaultAssembly;
}
}
catch
{
// Swallow errors in loading from the default context
}
}
/// <summary>
/// Load an assembly from a filepath.
/// </summary>
/// <param name="path">Assembly path.</param>
/// <returns>A loaded assembly.</returns>
public Assembly LoadAssemblyFromFilePath(string path)
var resolvedPath = this.dependencyResolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
if (!this.loadInMemory)
return this.LoadFromAssemblyPath(path);
using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var pdbPath = Path.ChangeExtension(path, ".pdb");
if (File.Exists(pdbPath))
{
using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return this.LoadFromStream(file, pdbFile);
}
return this.LoadFromStream(file);
return this.LoadAssemblyFromFilePath(resolvedPath);
}
/// <summary>
/// Load an assembly.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Loaded assembly.</returns>
protected override Assembly? Load(AssemblyName assemblyName)
// Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders)
// for $folder/$culture/$assemblyName.dll
// See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290
if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals(assemblyName.CultureName, "neutral"))
{
if (assemblyName.Name == null)
foreach (var resourceRoot in this.resourceRoots)
{
// not sure how to handle this case. It's technically possible.
return null;
}
if ((this.preferDefaultLoadContext || this.defaultAssemblies.Contains(assemblyName.Name)) && !this.privateAssemblies.Contains(assemblyName.Name))
{
// If default context is preferred, check first for types in the default context unless the dependency has been declared as private
try
var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
if (File.Exists(resourcePath))
{
var defaultAssembly = this.defaultLoadContext.LoadFromAssemblyName(assemblyName);
if (defaultAssembly != null)
{
// Older versions used to return null here such that returned assembly would be resolved from the default ALC.
// However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
// this context was built. As such, we simply return the Assembly from the user's chosen default load context.
return defaultAssembly;
}
}
catch
{
// Swallow errors in loading from the default context
}
}
var resolvedPath = this.dependencyResolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
return this.LoadAssemblyFromFilePath(resolvedPath);
}
// Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders)
// for $folder/$culture/$assemblyName.dll
// See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290
if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals(assemblyName.CultureName, "neutral"))
{
foreach (var resourceRoot in this.resourceRoots)
{
var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
if (File.Exists(resourcePath))
{
return this.LoadAssemblyFromFilePath(resourcePath);
}
}
return null;
}
if (this.managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
{
if (this.SearchForLibrary(library, out var path) && path != null)
{
return this.LoadAssemblyFromFilePath(path);
}
}
else
{
// if an assembly was not listed in the list of known assemblies,
// fallback to the load context base directory
var dllName = assemblyName.Name + ".dll";
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, dllName);
if (File.Exists(localFile))
{
return this.LoadAssemblyFromFilePath(localFile);
}
return this.LoadAssemblyFromFilePath(resourcePath);
}
}
return null;
}
/// <summary>
/// Loads the unmanaged binary using configured list of native libraries.
/// </summary>
/// <param name="unmanagedDllName">Unmanaged DLL name.</param>
/// <returns>The unmanaged dll handle.</returns>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
if (this.managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
{
var resolvedPath = this.dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
if (this.SearchForLibrary(library, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
return this.LoadAssemblyFromFilePath(path);
}
foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
}
else
{
// if an assembly was not listed in the list of known assemblies,
// fallback to the load context base directory
var dllName = assemblyName.Name + ".dll";
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
if (this.nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
var localFile = Path.Combine(probingPath, dllName);
if (File.Exists(localFile))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")]
// This library treats the file name without the extension as the lookup name,
// so this loop is necessary to check if the unmanaged name matches a library
// when the file extension has been trimmed.
foreach (var suffix in PlatformInformation.NativeLibraryExtensions)
{
if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// check to see if there is a library entry for the library without the file extension
var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length);
if (this.nativeLibraries.TryGetValue(prefix + trimmedName, out library))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// fallback to native assets which match the file name in the plugin base directory
var prefixSuffixDllName = prefix + unmanagedDllName + suffix;
var prefixDllName = prefix + unmanagedDllName;
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, prefixSuffixDllName);
if (File.Exists(localFile))
{
return this.LoadUnmanagedDllFromResolvedPath(localFile);
}
var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
if (File.Exists(localFileWithoutSuffix))
{
return this.LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix);
}
}
}
}
return this.LoadAssemblyFromFilePath(localFile);
}
}
return base.LoadUnmanagedDll(unmanagedDllName);
}
private bool SearchForLibrary(ManagedLibrary library, out string? path)
return null;
}
/// <summary>
/// Loads the unmanaged binary using configured list of native libraries.
/// </summary>
/// <param name="unmanagedDllName">Unmanaged DLL name.</param>
/// <returns>The unmanaged dll handle.</returns>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var resolvedPath = this.dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
// 1. Check for in _basePath + app local path
var localFile = Path.Combine(this.basePath, library.AppLocalPath);
if (File.Exists(localFile))
return this.LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
}
foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
{
if (this.nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
{
path = localFile;
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")]
// This library treats the file name without the extension as the lookup name,
// so this loop is necessary to check if the unmanaged name matches a library
// when the file extension has been trimmed.
foreach (var suffix in PlatformInformation.NativeLibraryExtensions)
{
if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// check to see if there is a library entry for the library without the file extension
var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length);
if (this.nativeLibraries.TryGetValue(prefix + trimmedName, out library))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// fallback to native assets which match the file name in the plugin base directory
var prefixSuffixDllName = prefix + unmanagedDllName + suffix;
var prefixDllName = prefix + unmanagedDllName;
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, prefixSuffixDllName);
if (File.Exists(localFile))
{
return this.LoadUnmanagedDllFromResolvedPath(localFile);
}
var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
if (File.Exists(localFileWithoutSuffix))
{
return this.LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix);
}
}
}
}
}
}
return base.LoadUnmanagedDll(unmanagedDllName);
}
private bool SearchForLibrary(ManagedLibrary library, out string? path)
{
// 1. Check for in _basePath + app local path
var localFile = Path.Combine(this.basePath, library.AppLocalPath);
if (File.Exists(localFile))
{
path = localFile;
return true;
}
// 2. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
// 2. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
// 3. Search in base path
foreach (var ext in PlatformInformation.ManagedAssemblyExtensions)
{
var local = Path.Combine(this.basePath, library.Name.Name + ext);
if (File.Exists(local))
{
path = local;
return true;
}
}
path = null;
return false;
}
private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path)
// 3. Search in base path
foreach (var ext in PlatformInformation.ManagedAssemblyExtensions)
{
// 1. Search in base path
foreach (var ext in PlatformInformation.NativeLibraryExtensions)
{
var candidate = Path.Combine(this.basePath, $"{prefix}{library.Name}{ext}");
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
// 2. Search in base path + app local (for portable deployments of netcoreapp)
var local = Path.Combine(this.basePath, library.AppLocalPath);
var local = Path.Combine(this.basePath, library.Name.Name + ext);
if (File.Exists(local))
{
path = local;
return true;
}
// 3. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
path = null;
return false;
}
private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true)
{
if (normalizePath)
{
unmanagedDllPath = Path.GetFullPath(unmanagedDllPath);
}
path = null;
return false;
}
return this.shadowCopyNativeLibraries
? this.LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
: this.LoadUnmanagedDllFromPath(unmanagedDllPath);
private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path)
{
// 1. Search in base path
foreach (var ext in PlatformInformation.NativeLibraryExtensions)
{
var candidate = Path.Combine(this.basePath, $"{prefix}{library.Name}{ext}");
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
// 2. Search in base path + app local (for portable deployments of netcoreapp)
var local = Path.Combine(this.basePath, library.AppLocalPath);
if (File.Exists(local))
{
var shadowCopyDllPath = this.CreateShadowCopy(unmanagedDllPath);
return this.LoadUnmanagedDllFromPath(shadowCopyDllPath);
path = local;
return true;
}
private string CreateShadowCopy(string dllPath)
// 3. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
Directory.CreateDirectory(this.unmanagedDllShadowCopyDirectoryPath);
var dllFileName = Path.GetFileName(dllPath);
var shadowCopyPath = Path.Combine(this.unmanagedDllShadowCopyDirectoryPath, dllFileName);
if (!File.Exists(shadowCopyPath))
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
File.Copy(dllPath, shadowCopyPath);
path = candidate;
return true;
}
return shadowCopyPath;
}
private void OnUnloaded()
{
if (!this.shadowCopyNativeLibraries || !Directory.Exists(this.unmanagedDllShadowCopyDirectoryPath))
{
return;
}
path = null;
return false;
}
// Attempt to delete shadow copies
try
{
Directory.Delete(this.unmanagedDllShadowCopyDirectoryPath, recursive: true);
}
catch (Exception)
{
// Files might be locked by host process. Nothing we can do about it, I guess.
}
private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true)
{
if (normalizePath)
{
unmanagedDllPath = Path.GetFullPath(unmanagedDllPath);
}
return this.shadowCopyNativeLibraries
? this.LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
: this.LoadUnmanagedDllFromPath(unmanagedDllPath);
}
private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
{
var shadowCopyDllPath = this.CreateShadowCopy(unmanagedDllPath);
return this.LoadUnmanagedDllFromPath(shadowCopyDllPath);
}
private string CreateShadowCopy(string dllPath)
{
Directory.CreateDirectory(this.unmanagedDllShadowCopyDirectoryPath);
var dllFileName = Path.GetFileName(dllPath);
var shadowCopyPath = Path.Combine(this.unmanagedDllShadowCopyDirectoryPath, dllFileName);
if (!File.Exists(shadowCopyPath))
{
File.Copy(dllPath, shadowCopyPath);
}
return shadowCopyPath;
}
private void OnUnloaded()
{
if (!this.shadowCopyNativeLibraries || !Directory.Exists(this.unmanagedDllShadowCopyDirectoryPath))
{
return;
}
// Attempt to delete shadow copies
try
{
Directory.Delete(this.unmanagedDllShadowCopyDirectoryPath, recursive: true);
}
catch (Exception)
{
// Files might be locked by host process. Nothing we can do about it, I guess.
}
}
}

View file

@ -1,32 +1,31 @@
// Copyright (c) Nate McMaster, Dalamud team.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// Platform specific information.
/// </summary>
internal class PlatformInformation
{
/// <summary>
/// Platform specific information.
/// Gets a list of native OS specific library extensions.
/// </summary>
internal class PlatformInformation
public static string[] NativeLibraryExtensions => new[] { ".dll" };
/// <summary>
/// Gets a list of native OS specific library prefixes.
/// </summary>
public static string[] NativeLibraryPrefixes => new[] { string.Empty };
/// <summary>
/// Gets a list of native OS specific managed assembly extensions.
/// </summary>
public static string[] ManagedAssemblyExtensions => new[]
{
/// <summary>
/// Gets a list of native OS specific library extensions.
/// </summary>
public static string[] NativeLibraryExtensions => new[] { ".dll" };
/// <summary>
/// Gets a list of native OS specific library prefixes.
/// </summary>
public static string[] NativeLibraryPrefixes => new[] { string.Empty };
/// <summary>
/// Gets a list of native OS specific managed assembly extensions.
/// </summary>
public static string[] ManagedAssemblyExtensions => new[]
{
".dll",
".ni.dll",
".exe",
".ni.exe",
};
}
".dll",
".ni.dll",
".exe",
".ni.exe",
};
}

View file

@ -5,159 +5,158 @@ using System;
using System.Reflection;
using System.Runtime.Loader;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// This loader attempts to load binaries for execution (both managed assemblies and native libraries)
/// in the same way that .NET Core would if they were originally part of the .NET Core application.
/// <para>
/// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json)
/// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies.
/// The loader searches the plugin path, as well as any additionally specified paths, for binaries
/// which satisfy the plugin's requirements.
/// </para>
/// </summary>
internal class PluginLoader : IDisposable
{
private readonly LoaderConfig config;
private readonly AssemblyLoadContextBuilder contextBuilder;
private ManagedLoadContext context;
private volatile bool disposed;
/// <summary>
/// This loader attempts to load binaries for execution (both managed assemblies and native libraries)
/// in the same way that .NET Core would if they were originally part of the .NET Core application.
/// Initializes a new instance of the <see cref="PluginLoader"/> class.
/// </summary>
/// <param name="config">The configuration for the plugin.</param>
public PluginLoader(LoaderConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.contextBuilder = CreateLoadContextBuilder(config);
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
/// <summary>
/// Gets a value indicating whether this plugin is capable of being unloaded.
/// </summary>
public bool IsUnloadable
=> this.context.IsCollectible;
/// <summary>
/// Gets the assembly load context.
/// </summary>
public AssemblyLoadContext LoadContext => this.context;
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action<LoaderConfig> configure)
{
if (configure == null)
throw new ArgumentNullException(nameof(configure));
var config = new LoaderConfig(assemblyFile);
configure(config);
return new PluginLoader(config);
}
/// <summary>
/// The unloads and reloads the plugin assemblies.
/// This method throws if <see cref="IsUnloadable" /> is <c>false</c>.
/// </summary>
public void Reload()
{
this.EnsureNotDisposed();
if (!this.IsUnloadable)
{
throw new InvalidOperationException("Reload cannot be used because IsUnloadable is false");
}
this.context.Unload();
this.context = (ManagedLoadContext)this.contextBuilder.Build();
GC.Collect();
GC.WaitForPendingFinalizers();
}
/// <summary>
/// Load the main assembly for the plugin.
/// </summary>
/// <returns>The assembly.</returns>
public Assembly LoadDefaultAssembly()
{
this.EnsureNotDisposed();
return this.context.LoadAssemblyFromFilePath(this.config.MainAssemblyPath);
}
/// <summary>
/// Sets the scope used by some System.Reflection APIs which might trigger assembly loading.
/// <para>
/// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json)
/// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies.
/// The loader searches the plugin path, as well as any additionally specified paths, for binaries
/// which satisfy the plugin's requirements.
/// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details.
/// </para>
/// </summary>
internal class PluginLoader : IDisposable
/// <returns>A contextual reflection scope.</returns>
public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
=> this.context.EnterContextualReflection();
/// <summary>
/// Disposes the plugin loader. This only does something if <see cref="IsUnloadable" /> is true.
/// When true, this will unload assemblies which which were loaded during the lifetime
/// of the plugin.
/// </summary>
public void Dispose()
{
private readonly LoaderConfig config;
private readonly AssemblyLoadContextBuilder contextBuilder;
private ManagedLoadContext context;
private volatile bool disposed;
if (this.disposed)
return;
/// <summary>
/// Initializes a new instance of the <see cref="PluginLoader"/> class.
/// </summary>
/// <param name="config">The configuration for the plugin.</param>
public PluginLoader(LoaderConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.contextBuilder = CreateLoadContextBuilder(config);
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
/// <summary>
/// Gets a value indicating whether this plugin is capable of being unloaded.
/// </summary>
public bool IsUnloadable
=> this.context.IsCollectible;
/// <summary>
/// Gets the assembly load context.
/// </summary>
public AssemblyLoadContext LoadContext => this.context;
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action<LoaderConfig> configure)
{
if (configure == null)
throw new ArgumentNullException(nameof(configure));
var config = new LoaderConfig(assemblyFile);
configure(config);
return new PluginLoader(config);
}
/// <summary>
/// The unloads and reloads the plugin assemblies.
/// This method throws if <see cref="IsUnloadable" /> is <c>false</c>.
/// </summary>
public void Reload()
{
this.EnsureNotDisposed();
if (!this.IsUnloadable)
{
throw new InvalidOperationException("Reload cannot be used because IsUnloadable is false");
}
this.disposed = true;
if (this.context.IsCollectible)
this.context.Unload();
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
private static AssemblyLoadContextBuilder CreateLoadContextBuilder(LoaderConfig config)
{
var builder = new AssemblyLoadContextBuilder();
/// <summary>
/// Load the main assembly for the plugin.
/// </summary>
/// <returns>The assembly.</returns>
public Assembly LoadDefaultAssembly()
builder.SetMainAssemblyPath(config.MainAssemblyPath);
builder.SetDefaultContext(config.DefaultContext);
foreach (var ext in config.PrivateAssemblies)
{
this.EnsureNotDisposed();
return this.context.LoadAssemblyFromFilePath(this.config.MainAssemblyPath);
builder.PreferLoadContextAssembly(ext);
}
/// <summary>
/// Sets the scope used by some System.Reflection APIs which might trigger assembly loading.
/// <para>
/// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details.
/// </para>
/// </summary>
/// <returns>A contextual reflection scope.</returns>
public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
=> this.context.EnterContextualReflection();
/// <summary>
/// Disposes the plugin loader. This only does something if <see cref="IsUnloadable" /> is true.
/// When true, this will unload assemblies which which were loaded during the lifetime
/// of the plugin.
/// </summary>
public void Dispose()
if (config.PreferSharedTypes)
{
if (this.disposed)
return;
this.disposed = true;
if (this.context.IsCollectible)
this.context.Unload();
builder.PreferDefaultLoadContext(true);
}
private static AssemblyLoadContextBuilder CreateLoadContextBuilder(LoaderConfig config)
if (config.IsUnloadable)
{
var builder = new AssemblyLoadContextBuilder();
builder.SetMainAssemblyPath(config.MainAssemblyPath);
builder.SetDefaultContext(config.DefaultContext);
foreach (var ext in config.PrivateAssemblies)
{
builder.PreferLoadContextAssembly(ext);
}
if (config.PreferSharedTypes)
{
builder.PreferDefaultLoadContext(true);
}
if (config.IsUnloadable)
{
builder.EnableUnloading();
}
if (config.LoadInMemory)
{
builder.PreloadAssembliesIntoMemory();
builder.ShadowCopyNativeLibraries();
}
foreach (var assemblyName in config.SharedAssemblies)
{
builder.PreferDefaultLoadContextAssembly(assemblyName);
}
return builder;
builder.EnableUnloading();
}
private void EnsureNotDisposed()
if (config.LoadInMemory)
{
if (this.disposed)
throw new ObjectDisposedException(nameof(PluginLoader));
builder.PreloadAssembliesIntoMemory();
builder.ShadowCopyNativeLibraries();
}
foreach (var assemblyName in config.SharedAssemblies)
{
builder.PreferDefaultLoadContextAssembly(assemblyName);
}
return builder;
}
private void EnsureNotDisposed()
{
if (this.disposed)
throw new ObjectDisposedException(nameof(PluginLoader));
}
}

View file

@ -45,6 +45,15 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
public const int PluginWaitBeforeFreeDefault = 500;
private const string DevPluginsDisclaimerFilename = "DONT_USE_THIS_FOLDER.txt";
private const string DevPluginsDisclaimerText = @"Hey!
The devPlugins folder is deprecated and will be removed soon. Please don't use it anymore for plugin development.
Instead, open the Dalamud settings and add the path to your plugins build output folder as a dev plugin location.
Remove your devPlugin from this folder.
Thanks and have fun!";
private static readonly ModuleLog Log = new("PLUGINM");
private readonly object pluginListLock = new();
@ -72,6 +81,10 @@ internal partial class PluginManager : IDisposable, IServiceType
if (!this.devPluginDirectory.Exists)
this.devPluginDirectory.Create();
var disclaimerFileName = Path.Combine(this.devPluginDirectory.FullName, DevPluginsDisclaimerFilename);
if (!File.Exists(disclaimerFileName))
File.WriteAllText(disclaimerFileName, DevPluginsDisclaimerText);
this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode || this.startInfo.NoLoadPlugins;
try
@ -93,7 +106,7 @@ internal partial class PluginManager : IDisposable, IServiceType
if (this.SafeMode)
{
this.configuration.PluginSafeMode = false;
this.configuration.Save();
this.configuration.QueueSave();
}
this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(this.startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs"));
@ -105,11 +118,13 @@ internal partial class PluginManager : IDisposable, IServiceType
throw new InvalidDataException("Couldn't deserialize banned plugins manifest.");
}
this.openInstallerWindowPluginChangelogsLink = Service<ChatGui>.Get().AddChatLinkHandler("Dalamud", 1003, (i, m) =>
this.openInstallerWindowPluginChangelogsLink = Service<ChatGui>.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerPluginChangelogs();
});
this.configuration.PluginTestingOptIns ??= new List<PluginTestingOptIn>();
this.ApplyPatches();
}
@ -173,6 +188,42 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
public bool LoadBannedPlugins { get; set; }
/// <summary>
/// Gets a value indicating whether the given repo manifest should be visible to the user.
/// </summary>
/// <param name="manifest">Repo manifest.</param>
/// <returns>If the manifest is visible.</returns>
public static bool IsManifestVisible(RemotePluginManifest manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
// Hidden by user
if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
return false;
// Hidden by manifest
return !manifest.IsHide;
}
/// <summary>
/// Check if a manifest even has an available testing version.
/// </summary>
/// <param name="manifest">The manifest to test.</param>
/// <returns>Whether or not a testing version is available.</returns>
public static bool HasTestingVersion(PluginManifest manifest)
{
var av = manifest.AssemblyVersion;
var tv = manifest.TestingAssemblyVersion;
var hasTv = tv != null;
if (hasTv)
{
return tv > av;
}
return false;
}
/// <summary>
/// Print to chat any plugin updates and whether they were successful.
/// </summary>
@ -217,51 +268,34 @@ internal partial class PluginManager : IDisposable, IServiceType
}
}
/// <summary>
/// For a given manifest, determine if the user opted into testing this plugin.
/// </summary>
/// <param name="manifest">Manifest to check.</param>
/// <returns>A value indicating whether testing should be used.</returns>
public bool HasTestingOptIn(PluginManifest manifest)
{
return this.configuration.PluginTestingOptIns!.Any(x => x.InternalName == manifest.InternalName);
}
/// <summary>
/// For a given manifest, determine if the testing version should be used over the normal version.
/// The higher of the two versions is calculated after checking other settings.
/// </summary>
/// <param name="manifest">Manifest to check.</param>
/// <returns>A value indicating whether testing should be used.</returns>
public static bool UseTesting(PluginManifest manifest)
public bool UseTesting(PluginManifest manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
if (!this.configuration.DoPluginTest)
return false;
if (!configuration.DoPluginTest)
if (!this.HasTestingOptIn(manifest))
return false;
if (manifest.IsTestingExclusive)
return true;
var av = manifest.AssemblyVersion;
var tv = manifest.TestingAssemblyVersion;
var hasTv = tv != null;
if (hasTv)
{
return tv > av;
}
return false;
}
/// <summary>
/// Gets a value indicating whether the given repo manifest should be visible to the user.
/// </summary>
/// <param name="manifest">Repo manifest.</param>
/// <returns>If the manifest is visible.</returns>
public static bool IsManifestVisible(RemotePluginManifest manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
// Hidden by user
if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
return false;
return true; // TODO temporary
// Hidden by manifest
return !manifest.IsHide;
return HasTestingVersion(manifest);
}
/// <inheritdoc/>
@ -364,6 +398,9 @@ internal partial class PluginManager : IDisposable, IServiceType
var manifest = LocalPluginManifest.Load(manifestFile);
if (manifest.IsTestingExclusive && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != manifest.InternalName))
this.configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(manifest.InternalName));
versionsDefs.Add(new PluginDef(dllFile, manifest, false));
}
catch (Exception ex)
@ -372,6 +409,8 @@ internal partial class PluginManager : IDisposable, IServiceType
}
}
this.configuration.QueueSave();
try
{
pluginDefs.Add(versionsDefs.OrderByDescending(x => x.Manifest!.EffectiveVersion).First());
@ -674,6 +713,14 @@ internal partial class PluginManager : IDisposable, IServiceType
{
Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})");
// Ensure that we have a testing opt-in for this plugin if we are installing a testing version
if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName))
{
// TODO: this isn't safe
this.configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(repoManifest.InternalName));
this.configuration.QueueSave();
}
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
@ -948,6 +995,7 @@ internal partial class PluginManager : IDisposable, IServiceType
versionDir.Delete(true);
continue;
}
var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
if (!dllFile.Exists)
{
@ -995,6 +1043,7 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <summary>
/// Update all non-dev plugins.
/// </summary>
/// <param name="ignoreDisabled">Ignore disabled plugins.</param>
/// <param name="dryRun">Perform a dry run, don't install anything.</param>
/// <returns>Success or failure and a list of updated plugin metadata.</returns>
public async Task<List<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun)
@ -1252,7 +1301,7 @@ internal partial class PluginManager : IDisposable, IServiceType
.Where(remoteManifest => remoteManifest.DalamudApiLevel == DalamudApiLevel)
.Select(remoteManifest =>
{
var useTesting = UseTesting(remoteManifest);
var useTesting = this.UseTesting(remoteManifest);
var candidateVersion = useTesting
? remoteManifest.TestingAssemblyVersion
: remoteManifest.AssemblyVersion;

View file

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Support;
using Dalamud.Utility.Timing;

View file

@ -37,7 +37,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings))
{
configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
configuration.Save();
configuration.QueueSave();
}
if (this.AutomaticReload)

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;

View file

@ -12,10 +12,12 @@ namespace Dalamud.Plugin.Internal.Types;
/// </summary>
internal record LocalPluginManifest : PluginManifest
{
/// <summary> "OFFICIAL" </summary>
/// <summary>
/// Flag indicating that a plugin was installed from the official repo.
/// </summary>
[JsonIgnore]
public const string FlagMainRepo = "OFFICIAL";
/// <summary> "DEVPLUGIN" </summary>
[JsonIgnore]
public const string FlagDevPlugin = "DEVPLUGIN";
@ -55,6 +57,11 @@ internal record LocalPluginManifest : PluginManifest
/// </summary>
public Version EffectiveVersion => this.Testing && this.TestingAssemblyVersion != null ? this.TestingAssemblyVersion : this.AssemblyVersion;
/// <summary>
/// Gets a value indicating whether this plugin is eligible for testing.
/// </summary>
public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion;
/// <summary>
/// Save a plugin manifest to file.
/// </summary>

View file

@ -137,9 +137,9 @@ internal record PluginManifest
/// <summary>
/// Gets the required Dalamud load step for this plugin to load. Takes precedence over LoadPriority.
/// Valid values are:
/// 0. During Framework.Tick, when drawing facilities are available
/// 1. During Framework.Tick
/// 2. No requirement
/// 0. During Framework.Tick, when drawing facilities are available.
/// 1. During Framework.Tick.
/// 2. No requirement.
/// </summary>
[JsonProperty]
public int LoadRequiredState { get; init; }
@ -157,7 +157,7 @@ internal record PluginManifest
public int LoadPriority { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// </summary>
[JsonProperty]
public bool CanUnloadAsync { get; init; }