Hermod
A cross-platform, modular and fully GDPR-compliant email archival solution!
Loading...
Searching...
No Matches
PluginRegistry.cs
Go to the documentation of this file.
1using System;
2
4
5 using Config;
6 using Core.Attributes;
7 using Core.Commands;
8 using Core.Exceptions;
9 using Serilog;
10
11 using System.Reflection;
12 using System.Reflection.Metadata;
13 using System.Reflection.PortableExecutable;
14
21 internal sealed partial class PluginRegistry {
22
23 public ILogger? AppLogger { get; internal set; } = null;
24
25 #region Singleton
26 private PluginRegistry() {
27 ConfigManager.Instance.ConfigChanged += ConfigManager_ConfigChanged;
28 ConfigManager.Instance.ConfigLoaded += ConfigManager_ConfigLoaded;
29 AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
30 }
31
32 private static PluginRegistry? _instance;
33
37 public static PluginRegistry Instance => _instance ??= new PluginRegistry();
38 #endregion
39
43 internal List<IPlugin> Plugins {
44 get {
45 List<IPlugin> plugins = new List<IPlugin>();
46 foreach (var pluginList in LoadedAssemblies.Select(x => x.Value.Select(y => y.Value))) {
47 plugins.AddRange(pluginList);
48 }
49
50 return plugins;
51 }
52 }
53
57 internal Dictionary<Assembly, Dictionary<Type, IPlugin>> LoadedAssemblies { get; } = new Dictionary<Assembly, Dictionary<Type, IPlugin>>();
58
59 internal List<PluginDelegator> PluginDelegators { get; } = new List<PluginDelegator>();
60
61 internal List<ICommand>? BuiltInCommands { get; set; } = null;
62
66 internal IPlugin? LastRegisteredPlugin { get; set; } = null;
67
74 internal void LoadPlugin(FileInfo pluginFile) {
75 AppLogger?.Information($"Attempting to load { pluginFile.FullName }...");
76 if (!pluginFile.Exists) { throw new FileNotFoundException("The file does not exist.", pluginFile.FullName); }
77 if (!IsAssembly(pluginFile)) { throw new NotAPluginException(pluginFile); }
78
79 AppLogger?.Information("Loading assembly...");
80 var assembly = Assembly.LoadFile(pluginFile.FullName);
81
82 List<Type> pluginTypes;
83 if (!ContainsPlugins(assembly, out pluginTypes)) {
84 throw new NotAPluginException(pluginFile);
85 }
86
87 AppLogger?.Information($"Found { pluginTypes.Count } plugins in assembly...");
88 foreach (var pluginType in pluginTypes) {
89 RegisterPlugin(ref assembly, pluginType);
90 }
91 }
92
98 internal bool IsAssembly(FileInfo pluginFile) {
99 try {
100 using (var fStream = pluginFile.OpenRead())
101 using (var peReader = new PEReader(fStream)) {
102 if (!peReader.HasMetadata) { return false; }
103
104 var metaDataReader = peReader.GetMetadataReader();
105 return metaDataReader.IsAssembly;
106 }
107 } catch (Exception) { return false; }
108 }
109
116 internal bool ContainsPlugins(Assembly assembly, out List<Type> pluginTypes) {
117 pluginTypes = new List<Type>();
118
119 foreach (var type in assembly.GetTypes()) {
120 var isSubclass = type.IsSubclassOf(typeof(IPlugin)) || type.IsSubclassOf(typeof(Plugin));
121 var attribute = type.GetCustomAttribute<PluginAttribute>();
122
123 if (isSubclass && attribute is not null) {
124 pluginTypes.Add(type);
125 }
126 }
127
128 return pluginTypes.Count > 0;
129 }
130
136 internal void RegisterPlugin(ref Assembly assembly, Type type) {
137 if (!LoadedAssemblies.ContainsKey(assembly)) {
138 AppLogger?.Debug($"Plugin assembly seems to be new; registering { assembly.GetName().FullName } for the first time!");
139 LoadedAssemblies.Add(assembly, new Dictionary<Type, IPlugin>());
140 } else if (LoadedAssemblies[assembly].ContainsKey(type)) {
141 AppLogger?.Error($"The plugin { type.Name } has already been loaded as a plugin! Silently ignoring...");
142 return;
143 }
144
145 try {
146 var plugin = Activator.CreateInstance(type) as IPlugin;
147 if (plugin is null) {
148 throw new PluginLoadException(type.Name);
149 }
150
151 LoadedAssemblies[assembly].Add(type, plugin);
152 var pluginDelegator = new PluginDelegator(plugin);
153 PluginDelegators.Add(pluginDelegator);
154
155 plugin = LoadedAssemblies[assembly][type];
156 plugin.OnLoad(pluginDelegator);
157
158 LastRegisteredPlugin = plugin;
159 AppLogger?.Information($"Loaded plugin { plugin.PluginName } { plugin.PluginVersion.ToString() }");
160 } catch (Exception ex) {
161 AppLogger?.Error("Failed to load plugin from assembly!");
162 AppLogger?.Error($"Error: { ex.Message }");
163
164 AppLogger?.Warning("Deregisterung plugin...");
165 DeregisterPlugin(assembly, type);
166
167 throw;
168 }
169 }
170
171 internal void DeregisterPlugin(Assembly assembly, Type type) {
172 if (LoadedAssemblies is null || LoadedAssemblies?.Count == 0) { return; }
173
174 var pluginAssembly = LoadedAssemblies?.FirstOrDefault(a => a.Key == assembly);
175 if (pluginAssembly is null) { return; }
176
177 pluginAssembly?.Value.Remove(type);
178 try {
179 PluginDelegators?.Remove(PluginDelegators.First(pd => pd.Plugin?.GetType() == type));
180 } catch { /* if an exception occurs, no PluginDelegator instance was loaded. */ }
181
182 if (pluginAssembly?.Value.Count == 0) {
183 LoadedAssemblies?.Remove(assembly);
184 }
185 }
186
187 private void ConfigManager_ConfigLoaded(object? sender, ConfigLoadedEventArgs e) {
188 foreach (var plugin in Plugins) {
189 plugin.OnConfigLoaded();
190 }
191 }
192
193 private void ConfigManager_ConfigChanged(object? sender, ConfigChangedEventArgs e) {
194 foreach (var plugin in Plugins) {
195 plugin.OnConfigChanged(e);
196 }
197 }
198
199 private Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args) {
200 AppLogger?.Warning("Failed to resolve assembly {argname}", args.Name);
201 var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies()?.Where(x => x.FullName == args.RequestingAssembly?.FullName)?.FirstOrDefault();
202 if (loadedAssembly is not null) {
203 return loadedAssembly;
204 }
205
206 var folderPath = Path.GetDirectoryName(args.RequestingAssembly?.Location);
207 var asmName = new AssemblyName(args.Name);
208 if (folderPath is null || asmName.Name is null) {
209 AppLogger?.Error("Could not find assembly in {location}", folderPath);
210 return default;
211 }
212
213 var rawPath = Path.Combine(folderPath, asmName.Name);
214 var asmPath = rawPath + ".dll";
215
216 if (!File.Exists(asmPath)) {
217 AppLogger?.Warning("Could not find {asmPath}! Is it an executable?", asmPath);
218 asmPath = rawPath + ".exe";
219 if (!File.Exists(asmPath)) {
220 AppLogger?.Warning("Could not find {asmPath}!", asmPath);
221 return default;
222 }
223 }
224
225 return Assembly.LoadFrom(asmPath);
226 }
227
232 internal List<ICommand> GetAllCommands() {
233 var list = BuiltInCommands ?? new List<ICommand>();
234
235 Plugins.ForEach(p => list.AddRange(p.PluginCommands));
236
237 return list;
238 }
239
240 }
241}
242
Event arguments for when a configuration has changed.
Plugin attribute, contains metadata about a given plugin.
Exception class that is thrown when an attempt is made to load a plugin which is not in an assembly.
Exception class that is thrown when a plugin failed to load.
Allows delegating topics and command execution requests from plugins through Hermod to other plugins.
An abstract class for plugins with all the main features already implemented.
Definition: Plugin.cs:13
Handles the loading, unloading, and general management of plugins.
void ConfigManager_ConfigLoaded(object? sender, ConfigLoadedEventArgs e)
void DeregisterPlugin(Assembly assembly, Type type)
List< IPlugin > Plugins
Gets a list of all loaded IPlugin instances.
bool ContainsPlugins(Assembly assembly, out List< Type > pluginTypes)
Gets a value indicating whether or not a given assembly contains members inheriting from IPlugin.
void ConfigManager_ConfigChanged(object? sender, ConfigChangedEventArgs e)
void RegisterPlugin(ref Assembly assembly, Type type)
Internally registers an IPlugin class and calls the IPlugin.OnLoad(Serilog.ILogger) method once loade...
Dictionary< Assembly, Dictionary< Type, IPlugin > > LoadedAssemblies
A Dictionary<Assembly, List<IPlugin>> containing all loaded assemblies and plugins contained within.
bool IsAssembly(FileInfo pluginFile)
Gets a value indicating whether or not a give file is a valid assembly or not.
List< PluginDelegator > PluginDelegators
static ? PluginRegistry _instance
IPlugin? LastRegisteredPlugin
Gets or sets the last IPlugin to be registered.
Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
void LoadPlugin(FileInfo pluginFile)
Loads one or plugins from an Assembly on disk.
static PluginRegistry Instance
Gets the current instance of this object.
List< ICommand > GetAllCommands()
Gets a list containing all ICommand instances known to the application at the current time.
Basic contract between Hermod and any plugins.
Definition: IPlugin.cs:14