Skip to content
Merged
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
54 changes: 37 additions & 17 deletions LabApi/Loader/Features/Misc/AssemblyUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static IEnumerable<string> GetLoadedAssemblies()
{
return AppDomain.CurrentDomain.GetAssemblies().Select(NameSelector);
// We will use this selector to format the assembly names to the format we want.
string NameSelector(Assembly assembly) => FormatAssemblyName(assembly.GetName());
static string NameSelector(Assembly assembly) => FormatAssemblyName(assembly.GetName());
}

/// <summary>
Expand All @@ -76,7 +76,7 @@ public static IEnumerable<string> GetLoadedAssemblies()
/// <returns>A formatted <see cref="IEnumerable{T}"/> with the missing dependencies.</returns>
public static IEnumerable<string> GetMissingDependencies(Assembly assembly)
{
IEnumerable<string> loadedAssemblies = GetLoadedAssemblies();
HashSet<string> loadedAssemblies = GetLoadedAssemblies().ToHashSet();
// Using the same format, we will get the missing dependencies.
return assembly.GetReferencedAssemblies().Select(FormatAssemblyName).Where(name => !loadedAssemblies.Contains(name));
}
Expand All @@ -85,6 +85,7 @@ public static IEnumerable<string> GetMissingDependencies(Assembly assembly)
/// Resolves embedded resources from the specified <see cref="Assembly"/>.
/// </summary>
/// <param name="assembly">The assembly to resolve embedded resources from.</param>
/// <remarks>This method loads each DLL if no such assembly with the same name has been loaded into the current AppDomain yet.</remarks>
public static void ResolveEmbeddedResources(Assembly assembly)
{
const string dllExtension = ".dll";
Expand All @@ -110,39 +111,41 @@ public static void ResolveEmbeddedResources(Assembly assembly)
}

/// <summary>
/// Loads an embedded dll from the specified <see cref="Assembly"/>.
/// Loads an embedded dll from the specified <see cref="Assembly"/> if no such assembly with the same name has been loaded.
/// </summary>
/// <param name="target">The assembly to load the embedded dll from.</param>
/// <param name="name">The resource name of the embedded dll.</param>
/// <remarks>The name check only checks for the <see cref="AssemblyName.Name"/> of the <see cref="AssemblyName"/>.</remarks>
public static void LoadEmbeddedDll(Assembly target, string name)
{
// We try to get the data stream of the specified resource name.
if (!TryGetDataStream(target, name, out Stream? dataStream))
return;

// We copy the data stream to a memory stream and load the assembly from the memory stream.
using MemoryStream stream = new();
dataStream.CopyTo(stream);
Assembly.Load(stream.ToArray());
string path = Path.GetTempFileName();
using (FileStream file = File.Create(path))
{
dataStream.CopyTo(file);
}
LoadAssemblyIfMissing(path);
}

/// <summary>
/// Loads a compressed embedded dll from the specified <see cref="Assembly"/>.
/// Loads a compressed embedded dll from the specified <see cref="Assembly"/> if no such assembly with the same name has been loaded.
/// </summary>
/// <param name="target">The assembly to load the compressed embedded dll from.</param>
/// <param name="name">The resource name of the compressed embedded dll.</param>
/// <remarks>The name check only checks for the <see cref="AssemblyName.Name"/> of the <see cref="AssemblyName"/>.</remarks>
public static void LoadCompressedEmbeddedDll(Assembly target, string name)
{
// We try to get the data stream of the specified resource name.
if (!TryGetDataStream(target, name, out Stream? dataStream))
return;

// We decompress the data stream and load the assembly from the memory stream.
using DeflateStream stream = new(dataStream, CompressionMode.Decompress);
// We use a memory stream to load the assembly from the decompressed data stream.
using MemoryStream memStream = new();
stream.CopyTo(memStream);
Assembly.Load(memStream.ToArray());
string path = Path.GetTempFileName();
using (DeflateStream stream = new(dataStream, CompressionMode.Decompress))
using (FileStream file = File.Create(path))
{
stream.CopyTo(file);
}
LoadAssemblyIfMissing(path);
}

/// <summary>
Expand Down Expand Up @@ -180,4 +183,21 @@ public static bool TryGetLoadedAssembly(this Plugin plugin, out Assembly assembl

// Used for missing assembly comparisons.
private static string FormatAssemblyName(AssemblyName assemblyName) => $"{assemblyName.Name} v{assemblyName.Version}";

private static void LoadAssemblyIfMissing(string path)
{
try
{
AssemblyName? name = AssemblyName.GetAssemblyName(path);
// only check name to avoid identity problems
if (name != null && AppDomain.CurrentDomain.GetAssemblies().All(e => e.GetName().Name != name.Name))
{
PluginLoader.Dependencies.Add(Assembly.Load(File.ReadAllBytes(path)));
}
}
finally
{
File.Delete(path);
}
}
}
75 changes: 47 additions & 28 deletions LabApi/Loader/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public static void LoadAllPlugins()
/// <param name="files">The collection of assemblies to load.</param>
public static void LoadPlugins(IEnumerable<FileInfo> files)
{
List<(FileInfo File, Assembly Assembly)> plugins = [];
List<(string Path, Assembly Assembly)> plugins = [];

foreach (FileInfo file in files)
{
Expand All @@ -177,8 +177,7 @@ public static void LoadPlugins(IEnumerable<FileInfo> files)
? Assembly.Load(File.ReadAllBytes(file.FullName), File.ReadAllBytes(pdb.FullName))
// Otherwise, we load the assembly without any debug information.
: Assembly.Load(File.ReadAllBytes(file.FullName));

plugins.Add((file, pluginAssembly));
plugins.Add((file.FullName, pluginAssembly));
}
catch (Exception e)
{
Expand All @@ -187,42 +186,32 @@ public static void LoadPlugins(IEnumerable<FileInfo> files)
}
}

foreach ((FileInfo file, Assembly assembly) in plugins)
foreach ((string path, Assembly assembly) in plugins)
{
try
{
// If the assembly has missing dependencies, we skip it.
if (AssemblyUtils.HasMissingDependencies(assembly, file.FullName, out Type[]? types))
continue;

InstantiatePlugins(types, assembly, file.FullName);
AssemblyUtils.ResolveEmbeddedResources(assembly);
}
catch (Exception e)
{
Logger.Error($"{LoggerPrefix} Couldn't load the plugin inside '{file.FullName}'");
Logger.Error($"{LoggerPrefix} Failed to resolve embedded resources for assembly '{path}'");
LogMissingDependencies(assembly);
Logger.Error(e);
}
}
}

private static void InstantiatePlugins(Type[] types, Assembly assembly, string filePath)
{
foreach (Type type in types)
foreach ((string path, Assembly assembly) in plugins)
{
// We check if the type is derived from Plugin.
if (!type.IsSubclassOf(typeof(Plugin)))
continue;

// We create an instance of the type and check if it was successfully created.
if (Activator.CreateInstance(type) is not Plugin plugin)
continue;

// Set the file path
plugin.FilePath = filePath;

// In that case, we add the plugin to the plugins list and log that it has been loaded.
Plugins.Add(plugin, assembly);
Logger.Info($"{LoggerPrefix} Successfully loaded {plugin.Name}");
try
{
InstantiatePlugins(assembly.GetTypes(), assembly, path);
}
catch (Exception e)
{
Logger.Error($"{LoggerPrefix} Couldn't load the plugin inside '{path}'");
LogMissingDependencies(assembly);
Logger.Error(e);
}
}
}

Expand Down Expand Up @@ -311,4 +300,34 @@ private static string ResolvePath(string path)
{
return path.Replace("$port", Server.Port.ToString());
}

private static void LogMissingDependencies(Assembly assembly)
{
IEnumerable<string> missing = AssemblyUtils.GetMissingDependencies(assembly);
if (missing.Any())
{
Logger.Error($"{LoggerPrefix} Missing dependencies:\n{string.Join("\n", missing.Select(static x => $"-\t {x}"))}");
}
}

private static void InstantiatePlugins(Type[] types, Assembly assembly, string filePath)
{
foreach (Type type in types)
{
// We check if the type is derived from Plugin.
if (!type.IsSubclassOf(typeof(Plugin)) || type.IsAbstract)
continue;

// We create an instance of the type and check if it was successfully created.
if (Activator.CreateInstance(type) is not Plugin plugin)
continue;

// Set the file path
plugin.FilePath = filePath;

// In that case, we add the plugin to the plugins list and log that it has been loaded.
Plugins.Add(plugin, assembly);
Logger.Info($"{LoggerPrefix} Successfully loaded {plugin.Name}");
}
}
}