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 PublicKeyTokens = new(); PublicKeyTokens.Add(typeof(String).Assembly.GetName().GetPublicKeyToken()!); // Framework Libraries PublicKeyTokens.Add(typeof(System.Runtime.GCSettings).Assembly.GetName().GetPublicKeyToken()!); KnownPublicKeyTokens = PublicKeyTokens.ToArray(); } public static readonly HashSet LibraryPaths = new(); public static IReadOnlyList Plugins => _plugins.AsReadOnly(); private static readonly List _plugins = new(); private static readonly List _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.GetName().Name) 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; } } } return null; } 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}'"); } } }