373 lines
15 KiB
C#
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}'");
|
|
}
|
|
}
|
|
} |