Extensible_Portfolio_Site/EPS.SDK/Plugins/PluginManager.cs

373 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
namespace ExtensiblePortfolioSite.SDK.Plugins
{
internal static class PluginManager
{
private readonly static String NativeLibraryPath;
private readonly static Byte[][] KnownPublicKeyTokens;
static PluginManager()
{
// Native/[Linux|Windows]/[X86|X64]/
StringBuilder NativePath = new(64);
NativePath.Append("Native");
NativePath.Append(Path.DirectorySeparatorChar);
NativePath.Append(RuntimeInformation.RuntimeIdentifier);
NativePath.Append(Path.DirectorySeparatorChar);
NativePath.Append(RuntimeInformation.ProcessArchitecture.ToString());
NativePath.Append(Path.DirectorySeparatorChar);
NativeLibraryPath = String.Intern(NativePath.ToString());
List<Byte[]> PublicKeyTokens = new();
PublicKeyTokens.Add(typeof(String).Assembly.GetName().GetPublicKeyToken()!); // Framework Libraries
KnownPublicKeyTokens = PublicKeyTokens.ToArray();
}
public static readonly HashSet<String> LibraryPaths = new();
public static IReadOnlyList<Plugin> Plugins => _plugins.AsReadOnly();
private static readonly List<Plugin> _plugins = new();
private static readonly List<Library> _libraries = new();
private static readonly List<(String, UnmanagedLibrary)> _unmanagedlibraries = new();
private static Boolean Initialized = false;
public static void LoadPlugins(string RootPath)
{
RootPath = Path.GetFullPath(RootPath);
if(!Directory.Exists(RootPath))
Directory.CreateDirectory(RootPath);
LibraryPaths.Add(RootPath);
foreach (String Dll in Directory.EnumerateFiles(RootPath, "*.dll", SearchOption.TopDirectoryOnly))
TryLoadPlugin(Path.GetFullPath(Dll, RootPath), out Plugin? _);
}
public static Boolean TryLoadPlugin(string AsmPath, out Plugin? plugin)
{
if (!File.Exists(AsmPath)) goto error;
AsmPath = Path.GetFullPath(AsmPath);
// check if assembly has an ESPPluginAttribute
Boolean IsPlugin = false;
using (FileStream FSTM = File.OpenRead(AsmPath))
{
Type EPSPluginAttribute_type = typeof(EPSPluginAttribute);
using PEReader PE = new(FSTM, PEStreamOptions.PrefetchMetadata);
MetadataReader MD = PE.GetMetadataReader();
IsPlugin = HasPluginAttribute(MD);
}
if (!IsPlugin) goto error;
lock (_plugins)
{
// ensure plugin is unique
foreach (Plugin p in _plugins)
if (p.Location == AsmPath)
goto error;
// load assembly
LibraryPaths.Add(Path.GetDirectoryName(AsmPath)!);
plugin = new Plugin(AsmPath);
_plugins.Add(plugin);
if (Initialized)
plugin.Init();
return true;
}
error:
plugin = null;
return false;
}
private static Boolean HasPluginAttribute(MetadataReader MD)
{
Type EPSPluginAttribute_type = typeof(EPSPluginAttribute);
foreach (CustomAttributeHandle handle in MD.GetAssemblyDefinition().GetCustomAttributes())
{
CustomAttribute Attr = MD.GetCustomAttribute(handle);
if (Attr.Constructor.Kind == HandleKind.MemberReference)
{
MemberReference Ctor = MD.GetMemberReference((MemberReferenceHandle)Attr.Constructor);
if (Ctor.Parent.Kind == HandleKind.TypeReference)
{
TypeReference AttrType = MD.GetTypeReference((TypeReferenceHandle)Ctor.Parent);
String AttrTypeName = MD.GetString(AttrType.Name);
String AttrTypeNamespace = MD.GetString(AttrType.Namespace);
if (
AttrType.ResolutionScope.Kind == HandleKind.AssemblyReference &&
AttrTypeName == EPSPluginAttribute_type.Name &&
AttrTypeNamespace == EPSPluginAttribute_type.Namespace
)
{
AssemblyReference AsmRef = MD.GetAssemblyReference((AssemblyReferenceHandle)AttrType.ResolutionScope);
String AsmName = MD.GetString(AsmRef.Name);
if (AsmName == EPSPluginAttribute_type.Assembly.FullName)
return true;
}
}
}
}
return false;
}
public static void InitializePlugins()
{
lock (_plugins)
{
Initialized = true;
foreach (Plugin plugin in _plugins)
plugin.Init();
}
}
public static Plugin? GetPlugin(String PluginName)
{
lock (_plugins)
foreach (Plugin p in _plugins)
if (p.Info.Name == PluginName)
return p;
return null;
}
internal static IPluginManagedLibrary? LoadLibrary(AssemblyName AsmName)
{
lock (LibraryPaths)
{
// Attempt to fetch pre-existing
lock (_libraries)
foreach (Library library in _libraries)
if (library.Assembly.GetName().Name == AsmName.Name)
return library;
lock (_plugins)
foreach (Plugin plugin in _plugins)
if (plugin.Assembly.GetName().Name == AsmName.Name)
return plugin;
Type EPSPluginAttribute_type = typeof(EPSPluginAttribute);
Boolean CheckDll(String Path, out Boolean IsPlugin)
{
using FileStream FSTM = File.OpenRead(Path);
using PEReader PE = new(FSTM, PEStreamOptions.PrefetchMetadata);
MetadataReader MD = PE.GetMetadataReader();
if (MD.GetAssemblyDefinition().GetAssemblyName().Name == AsmName.Name)
{
IsPlugin = HasPluginAttribute(MD);
return true;
}
IsPlugin = false;
return false;
}
foreach (String LibPath in LibraryPaths)
foreach (String file in Directory.EnumerateFiles(LibPath, "*.dll"))
if (CheckDll(file, out Boolean IsPlugin))
{
if (IsPlugin)
throw new InvalidOperationException($"Unable to Load Plugin File, '{file}', as it has not been explicitly loaded by the PluginManager");
else
{
Library Lib = new(Path.GetFullPath(file, LibPath));
_libraries.Add(Lib);
return Lib;
}
}
}
throw new FileNotFoundException($"Unable to load Assembly '{AsmName}'");
}
internal static IPluginUnmanagedLibrary? LoadUnmanagedLibrary(String Name)
{
Int32 Version = -1;
if (Name.StartsWith("lib"))
Name = Name[3..];
again:
int lastIndex = Name.LastIndexOf('.');
if (lastIndex != -1)
{
String s = Name[(lastIndex + 1)..];
switch (s)
{
case "dll":
case "so":
case "dynlib":
Name = Name[..lastIndex];
break;
default:
if (Int32.TryParse(s, out Version))
{
Name = Name[..lastIndex];
goto again;
}
break;
}
}
lastIndex = Name.LastIndexOf('-');
if (lastIndex != -1)
{
String s = Name[(lastIndex + 1)..];
if (Int32.TryParse(s, out Version))
Name = Name[..lastIndex];
}
/* [Load Order]
* OderId Pattern
* 1 *.dll
* 2 *.dynlib
* 3 *-[Version].so
* 4 *.so.[Version]
* 5 lib*-[Version].so
* 6 lib*.so.[Version]
* 7 *.so
* 8 lib*.so
*/
lock (LibraryPaths)
lock (_unmanagedlibraries)
{
// attempt to return already loaded library
foreach ((String, UnmanagedLibrary) lib in _unmanagedlibraries)
{
if (lib.Item1 == Name)
return lib.Item2;
}
// get possible entries
List<(int, String)> Entries = new();
foreach (String LibPath in LibraryPaths)
{
String Root = Path.Combine(LibPath, NativeLibraryPath);
foreach (String file in Directory.EnumerateFiles(Root))
{
if (file.StartsWith(Name) || (file.StartsWith("lib") && file[3..].StartsWith(Name)))
{
Int32 Order = -1;
String f = file;
Int32 v;
again2:
lastIndex = file.LastIndexOf('.');
String s = file[(lastIndex + 1)..];
switch (s)
{
case "dll":
Order = 1;
goto concat_f;
case "dynlib":
Order = 2;
concat_f:
f = f[..lastIndex];
break;
case "so":
f = f[..lastIndex];
if (f.StartsWith("lib"))
{
lastIndex = f.IndexOf('-');
if (Int32.TryParse(f[lastIndex..], out v))
if (v == Version || Version == -1)
Order = 5;
else
continue;
else
Order = Order == -2 ? 6 : 8;
}
else
{
lastIndex = f.IndexOf('-');
if (Int32.TryParse(f[lastIndex..], out v))
if (v == Version || Version == -1)
Order = 3;
else
continue;
else
Order = Order == -2 ? 4 : 7;
}
break;
default:
if (Int32.TryParse(s, out v))
{
if (v != Version)
continue;
Order = -2;
f = f[..lastIndex];
goto again2;
}
break;
}
if (Order > 0)
Entries.Add((Order, Path.GetFullPath(file, Root)));
}
}
}
if (Entries.Count > 0)
{
Int32 Order;
String BestPath;
(Order, BestPath) = Entries[0];
foreach ((Int32, String) Entry in Entries)
if (Entry.Item1 < Order)
(Order, BestPath) = Entry;
UnmanagedLibrary Lib = new UnmanagedLibrary(BestPath);
_unmanagedlibraries.Add((Name, Lib));
return Lib;
}
}
throw new FileNotFoundException($"Unable to load Native Dll '{Name}'");
}
internal static Assembly GetFixedAssembly(AssemblyName Name)
{
switch (Name.Name)
{
case "EPS.SDK":
return typeof(PluginManager).Assembly;
case "ExtensiblePortfolioSite":
return AssemblyLoadContext.Default.LoadFromAssemblyName(Name);
}
Byte[]? KeyToken = Name.GetPublicKeyToken();
if (KeyToken != null)
{
for (int x = 0; x < KnownPublicKeyTokens.Length; x++)
{
Boolean Allow = true;
Byte[] PubKeyToken = KnownPublicKeyTokens[x];
for (int y = 0; y < PubKeyToken.Length; y++)
if (PubKeyToken[y] != KeyToken[y])
{
Allow = false;
break;
}
if (Allow)
{
return AssemblyLoadContext.Default.LoadFromAssemblyName(Name);
}
}
}
// we want to throw when its trying to load something we don't explicitly allow!
throw new FileNotFoundException($"Unable to Load Assembly '{Name}'");
}
}
}