Free cookie consent management tool by TermsFeed Policy Generator

source: branches/2924_DotNetCoreMigration/HeuristicLab.PluginInfrastructure/3.3/Isolation/PluginLoader.cs @ 16985

Last change on this file since 16985 was 16984, checked in by dpiringe, 5 years ago

#2924:

  • merged projects HeuristicLab.PluginInfrastructure.Runner and HeuristicLab.PluginInfrastructure
  • applied changes of code reviews (13.05.2019 and 22.05.2019) -> the old Runner is now RunnerHost and uses a Runner, which is executed on the child process
  • added Type GetType(string) to IApplicationManager and implemented it for LightweightApplicationManager
  • removed IActivator and IActivatorContext
  • deleted unused types like PluginDescriptionIterator
File size: 22.5 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Reflection;
6using System.Security;
7using System.Text;
8
9namespace HeuristicLab.PluginInfrastructure {
10  public class PluginLoader : IPluginLoader {
11
12    /// <summary>
13    /// Helper class for depedency metadata.
14    /// </summary>
15    private class PluginDependency {
16      public string Name { get; private set; }
17      public Version Version { get; private set; }
18      public PluginDependency(string name, Version version) {
19        this.Name = name;
20        this.Version = version;
21      }
22    }
23
24    #region Vars
25    private IList<IPlugin> plugins = new List<IPlugin>();
26    private IList<IApplication> applications = new List<IApplication>();
27    private IAssemblyLoader assemblyLoader = null;
28    private Dictionary<PluginDescription, IEnumerable<PluginDependency>> pluginDependencies = new Dictionary<PluginDescription, IEnumerable<PluginDependency>>();
29    #endregion
30
31    #region Properties
32    public IEnumerable<IPlugin> Plugins => plugins;
33
34    public IEnumerable<IApplication> Applications => applications;
35    #endregion
36
37    #region Constructors
38    public PluginLoader(IAssemblyLoader assemblyLoader) {
39      this.assemblyLoader = assemblyLoader;
40    }
41    #endregion
42
43    #region Discover Methods
44    public void LoadPlugins(IEnumerable<AssemblyInfo> assemblyInfos) {
45      assemblyLoader.LoadAssemblies(assemblyInfos);
46      DiscoverPlugins();
47      DiscoverApplications();
48    }
49    /// <summary>
50    /// Discovers all types of IPlugin of all loaded assemblies. Saves the found plugins in a list.
51    /// </summary>
52    private void DiscoverPlugins() {
53      // search plugins out of all types
54      string curDir = Directory.GetCurrentDirectory(); // save current working directory
55      foreach (var type in assemblyLoader.Types) {
56        if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface && !type.HasElementType) {
57          // to set the working directory to the assembly location, this is necessary for assemblies which load data in the OnLoad method by their own.
58          // example: HeuristicLabMathJaxPlugin
59          Directory.SetCurrentDirectory(type.Assembly.Location.Replace(Path.GetFileName(type.Assembly.Location), ""));
60          IPlugin p = (IPlugin)Activator.CreateInstance(type);
61          p.OnLoad();
62          plugins.Add(p);
63        }
64      }
65      Directory.SetCurrentDirectory(curDir); // set working directory to its base value
66    }
67
68    /// <summary>
69    /// Discovers all types of IApplication of all loaded assemblies. Saves the found applications in a list.
70    /// </summary>
71    private void DiscoverApplications() {
72      foreach (Type type in assemblyLoader.Types) {
73        if (typeof(IApplication).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface && !type.HasElementType) {
74          IApplication app = (IApplication)Activator.CreateInstance(type);
75          applications.Add(app);
76        }
77      }
78    }
79    #endregion
80       
81    public IEnumerable<AssemblyInfo> Validate(string basePath) {
82      IEnumerable<Assembly> assemblies = assemblyLoader.LoadAssemblies(basePath);
83
84      IList<PluginDescription> pluginDescriptions = GatherPluginDescriptions(assemblies, basePath);
85      CheckPluginFiles(pluginDescriptions, basePath);
86
87      // check if all plugin assemblies can be loaded
88      CheckPluginAssemblies(assemblies, pluginDescriptions);
89
90      // a full list of plugin descriptions is available now we can build the dependency tree
91      BuildDependencyTree(pluginDescriptions);
92
93      // check for dependency cycles
94      CheckPluginDependencyCycles(pluginDescriptions);
95
96      // 1st time recursively check if all necessary plugins are available and not disabled
97      // disable plugins with missing or disabled dependencies
98      // to prevent that plugins with missing dependencies are loaded into the execution context
99      // in the next step
100      CheckPluginDependencies(pluginDescriptions);
101
102      // build assemblyInfo list:
103      // 1.) iterate through pluginDescriptions and pick all not disabled plugins
104      // 2.) iterate through the assemblyLocations, saved in a description
105      // 3.) iterate thorugh all loaded assemblies
106      // 4.) if the location of an assemblies and a pluginDescriptions match -> create new AssemblyInfo and add it to a list
107      IList<AssemblyInfo> list = new List<AssemblyInfo>();
108      foreach (var desc in pluginDescriptions) {
109        if (desc.PluginState != PluginState.Disabled) {
110          foreach (var loc in desc.AssemblyLocations) {
111            foreach (var asm in assemblies) {
112              if (string.Equals(Path.GetFullPath(asm.Location), Path.GetFullPath(loc), StringComparison.CurrentCultureIgnoreCase))
113                list.Add(new AssemblyInfo() { Path = new UniPath(asm.Location), Name = asm.FullName });
114            }
115          }
116        }
117      }
118
119      return list;
120    }
121
122    #region oldCode
123    /// <summary>
124    /// Checks if all plugin assemblies can be loaded. If an assembly can't be loaded the plugin is disabled.
125    /// </summary>
126    /// <param name="pluginDescriptions"></param>
127    private void CheckPluginAssemblies(IEnumerable<Assembly> assemblies, IEnumerable<PluginDescription> pluginDescriptions) {
128      foreach (var desc in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
129        try {
130          var missingAssemblies = new List<string>();
131          foreach (var asmLocation in desc.AssemblyLocations) {
132            // the assembly must have been loaded in ReflectionOnlyDlls
133            // so we simply determine the name of the assembly and try to find it in the cache of loaded assemblies
134            var asmName = AssemblyName.GetAssemblyName(asmLocation);
135            if (!assemblies.Select(x => x.GetName().FullName).Contains(asmName.FullName)) {
136              missingAssemblies.Add(asmName.FullName);
137            }
138          }
139          if (missingAssemblies.Count > 0) {
140            StringBuilder errorStrBuiler = new StringBuilder();
141            errorStrBuiler.AppendLine("Missing assemblies:");
142            foreach (string missingAsm in missingAssemblies) {
143              errorStrBuiler.AppendLine(missingAsm);
144            }
145            desc.Disable(errorStrBuiler.ToString());
146          }
147        } catch (BadImageFormatException ex) {
148          // disable the plugin
149          desc.Disable("Problem while loading plugin assemblies:" + Environment.NewLine + "BadImageFormatException: " + ex.Message);
150        } catch (FileNotFoundException ex) {
151          // disable the plugin
152          desc.Disable("Problem while loading plugin assemblies:" + Environment.NewLine + "FileNotFoundException: " + ex.Message);
153        } catch (FileLoadException ex) {
154          // disable the plugin
155          desc.Disable("Problem while loading plugin assemblies:" + Environment.NewLine + "FileLoadException: " + ex.Message);
156        } catch (ArgumentException ex) {
157          // disable the plugin
158          desc.Disable("Problem while loading plugin assemblies:" + Environment.NewLine + "ArgumentException: " + ex.Message);
159        } catch (SecurityException ex) {
160          // disable the plugin
161          desc.Disable("Problem while loading plugin assemblies:" + Environment.NewLine + "SecurityException: " + ex.Message);
162        }
163      }
164    }
165
166
167    // find all types implementing IPlugin in the reflectionOnlyAssemblies and create a list of plugin descriptions
168    // the dependencies in the plugin descriptions are not yet set correctly because we need to create
169    // the full list of all plugin descriptions first
170    private IList<PluginDescription> GatherPluginDescriptions(IEnumerable<Assembly> assemblies, string basePath) {
171      List<PluginDescription> pluginDescriptions = new List<PluginDescription>();
172      foreach (Assembly assembly in assemblies) {
173        // GetExportedTypes throws FileNotFoundException when a referenced assembly
174        // of the current assembly is missing.
175        try {
176          // if there is a type that implements IPlugin
177          // use AssemblyQualifiedName to compare the types because we can't directly
178          // compare ReflectionOnly types and execution types
179
180          var assemblyPluginDescriptions = from t in assembly.GetExportedTypes()
181                                           where !t.IsAbstract && t.GetInterfaces()
182                                           .Any(x => x.AssemblyQualifiedName == typeof(IPlugin).AssemblyQualifiedName)
183                                           select GetPluginDescription(t, basePath);
184          pluginDescriptions.AddRange(assemblyPluginDescriptions);
185        }
186        // ignore exceptions. Just don't yield a plugin description when an exception is thrown
187        catch (FileNotFoundException) {
188        } catch (FileLoadException) {
189        } catch (InvalidPluginException) {
190        } catch (TypeLoadException) {
191        } catch (MissingMemberException) {
192        }
193      }
194      return pluginDescriptions;
195    }
196
197    // checks if all declared plugin files are actually available and disables plugins with missing files
198    private void CheckPluginFiles(IEnumerable<PluginDescription> pluginDescriptions, string basePath) {
199      foreach (PluginDescription desc in pluginDescriptions) {
200        IEnumerable<string> missingFiles;
201        if (ArePluginFilesMissing(desc, basePath, out missingFiles)) {
202          StringBuilder errorStrBuilder = new StringBuilder();
203          errorStrBuilder.AppendLine("Missing files:");
204          foreach (string fileName in missingFiles) {
205            errorStrBuilder.AppendLine(fileName);
206          }
207          desc.Disable(errorStrBuilder.ToString());
208        }
209      }
210    }
211
212    private bool ArePluginFilesMissing(PluginDescription pluginDescription, string basePath, out IEnumerable<string> missingFiles) {
213      List<string> missing = new List<string>();
214      foreach (string filename in pluginDescription.Files.Select(x => x.Name)) {
215        if (!FileLiesInDirectory(basePath, filename) ||
216          !File.Exists(filename)) {
217          missing.Add(filename);
218        }
219      }
220      missingFiles = missing;
221      return missing.Count > 0;
222    }
223
224    private static bool FileLiesInDirectory(string dir, string fileName) {
225      var basePath = Path.GetFullPath(dir);
226      return Path.GetFullPath(fileName).StartsWith(basePath);
227    }
228
229    /// <summary>
230    /// Extracts plugin information for this type.
231    /// Reads plugin name, list and type of files and dependencies of the plugin. This information is necessary for
232    /// plugin dependency checking before plugin activation.
233    /// </summary>
234    /// <param name="pluginType"></param>
235    private PluginDescription GetPluginDescription(Type pluginType, string basePath) {
236
237      string pluginName, pluginDescription, pluginVersion;
238      string contactName, contactAddress;
239      GetPluginMetaData(pluginType, out pluginName, out pluginDescription, out pluginVersion);
240      GetPluginContactMetaData(pluginType, out contactName, out contactAddress);
241      var pluginFiles = GetPluginFilesMetaData(pluginType, basePath);
242      var pluginDependencies = GetPluginDependencyMetaData(pluginType);
243
244      // minimal sanity check of the attribute values
245      if (!string.IsNullOrEmpty(pluginName) &&
246          pluginFiles.Count() > 0 &&                                 // at least one file
247          pluginFiles.Any(f => f.Type == PluginFileType.Assembly)) { // at least one assembly
248        // create a temporary PluginDescription that contains the attribute values
249        PluginDescription info = new PluginDescription();
250        info.Name = pluginName;
251        info.Description = pluginDescription;
252        info.Version = new Version(pluginVersion);
253        info.ContactName = contactName;
254        info.ContactEmail = contactAddress;
255        info.LicenseText = ReadLicenseFiles(pluginFiles);
256        info.AddFiles(pluginFiles);
257
258        this.pluginDependencies[info] = pluginDependencies;
259        return info;
260      } else {
261        throw new InvalidPluginException("Invalid metadata in plugin " + pluginType.ToString());
262      }
263    }
264
265    private string ReadLicenseFiles(IEnumerable<PluginFile> pluginFiles) {
266      // combine the contents of all plugin files
267      var licenseFiles = from file in pluginFiles
268                         where file.Type == PluginFileType.License
269                         select file;
270      if (licenseFiles.Count() == 0) return string.Empty;
271      StringBuilder licenseTextBuilder = new StringBuilder();
272      licenseTextBuilder.AppendLine(File.ReadAllText(licenseFiles.First().Name));
273      foreach (var licenseFile in licenseFiles.Skip(1)) {
274        licenseTextBuilder.AppendLine().AppendLine(); // leave some empty space between multiple license files
275        licenseTextBuilder.AppendLine(File.ReadAllText(licenseFile.Name));
276      }
277      return licenseTextBuilder.ToString();
278    }
279
280    private static IEnumerable<PluginDependency> GetPluginDependencyMetaData(Type pluginType) {
281      // get all attributes of type PluginDependency
282      var dependencyAttributes = pluginType.GetCustomAttributes<PluginDependencyAttribute>();
283      /*from attr in CustomAttributeData.GetCustomAttributes(pluginType)
284                               where IsAttributeDataForType(attr, typeof(PluginDependencyAttribute))
285                               select attr;*/
286      foreach (var dependencyAttr in dependencyAttributes) {
287        string name = (string)dependencyAttr.Dependency; //ConstructorArguments[0].Value;
288        Version version = new Version("0.0.0.0"); // default version
289        // check if version is given for now
290        // later when the constructor of PluginDependencyAttribute with only one argument has been removed
291        // this conditional can be removed as well
292        if (dependencyAttr.Version != null) {
293          try {
294            version = dependencyAttr.Version;//new Version((string)dependencyAttr.ConstructorArguments[1].Value); // might throw FormatException
295          } catch (FormatException ex) {
296            throw new InvalidPluginException("Invalid version format of dependency " + name + " in plugin " + pluginType.ToString(), ex);
297          }
298        }
299        yield return new PluginDependency(name, version);
300      }
301    }
302
303    private static void GetPluginContactMetaData(Type pluginType, out string contactName, out string contactAddress) {
304      // get attribute of type ContactInformation if there is any
305      var contactInfoAttribute = (from attr in CustomAttributeData.GetCustomAttributes(pluginType)
306                                  where IsAttributeDataForType(attr, typeof(ContactInformationAttribute))
307                                  select attr).SingleOrDefault();
308
309      if (contactInfoAttribute != null) {
310        contactName = (string)contactInfoAttribute.ConstructorArguments[0].Value;
311        contactAddress = (string)contactInfoAttribute.ConstructorArguments[1].Value;
312      } else {
313        contactName = string.Empty;
314        contactAddress = string.Empty;
315      }
316    }
317
318    // not static because we need the BasePath property
319    private IEnumerable<PluginFile> GetPluginFilesMetaData(Type pluginType, string basePath) {
320      // get all attributes of type PluginFileAttribute
321      var pluginFileAttributes = from attr in CustomAttributeData.GetCustomAttributes(pluginType)
322                                 where IsAttributeDataForType(attr, typeof(PluginFileAttribute))
323                                 select attr;
324      foreach (var pluginFileAttribute in pluginFileAttributes) {
325        string pluginFileName = (string)pluginFileAttribute.ConstructorArguments[0].Value;
326        PluginFileType fileType = (PluginFileType)pluginFileAttribute.ConstructorArguments[1].Value;
327        yield return new PluginFile(Path.GetFullPath(Path.Combine(basePath, pluginFileName)), fileType);
328      }
329    }
330
331    private static void GetPluginMetaData(Type pluginType, out string pluginName, out string pluginDescription, out string pluginVersion) {
332      // there must be a single attribute of type PluginAttribute
333      var pluginMetaDataAttr = (from attr in CustomAttributeData.GetCustomAttributes(pluginType)
334                                where IsAttributeDataForType(attr, typeof(PluginAttribute))
335                                select attr).Single();
336
337      pluginName = (string)pluginMetaDataAttr.ConstructorArguments[0].Value;
338
339      // default description and version
340      pluginVersion = "0.0.0.0";
341      pluginDescription = string.Empty;
342      if (pluginMetaDataAttr.ConstructorArguments.Count() == 2) {
343        // if two arguments are given the second argument is the version
344        pluginVersion = (string)pluginMetaDataAttr.ConstructorArguments[1].Value;
345      } else if (pluginMetaDataAttr.ConstructorArguments.Count() == 3) {
346        // if three arguments are given the second argument is the description and the third is the version
347        pluginDescription = (string)pluginMetaDataAttr.ConstructorArguments[1].Value;
348        pluginVersion = (string)pluginMetaDataAttr.ConstructorArguments[2].Value;
349      }
350    }
351
352
353    private static bool IsAttributeDataForType(CustomAttributeData attributeData, Type attributeType) {
354      return attributeData.Constructor.DeclaringType.AssemblyQualifiedName == attributeType.AssemblyQualifiedName;
355    }
356
357    // builds a dependency tree of all plugin descriptions
358    // searches matching plugin descriptions based on the list of dependency names for each plugin
359    // and sets the dependencies in the plugin descriptions
360    private void BuildDependencyTree(IEnumerable<PluginDescription> pluginDescriptions) {
361      foreach (var desc in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
362        var missingDependencies = new List<PluginDependency>();
363        foreach (var dependency in pluginDependencies[desc]) {
364          var matchingDescriptions = from availablePlugin in pluginDescriptions
365                                     where availablePlugin.PluginState != PluginState.Disabled
366                                     where availablePlugin.Name == dependency.Name
367                                     where IsCompatiblePluginVersion(availablePlugin.Version, dependency.Version)
368                                     select availablePlugin;
369          if (matchingDescriptions.Count() > 0) {
370            desc.AddDependency(matchingDescriptions.First());
371          } else {
372            missingDependencies.Add(dependency);
373          }
374        }
375        // no plugin description that matches the dependencies are available => plugin is disabled
376        if (missingDependencies.Count > 0) {
377          StringBuilder errorStrBuilder = new StringBuilder();
378          errorStrBuilder.AppendLine("Missing dependencies:");
379          foreach (var missingDep in missingDependencies) {
380            errorStrBuilder.AppendLine(missingDep.Name + " " + missingDep.Version);
381          }
382          desc.Disable(errorStrBuilder.ToString());
383        }
384      }
385    }
386
387    /// <summary>
388    /// Checks if version <paramref name="available"/> is compatible to version <paramref name="requested"/>.
389    /// Note: the compatibility relation is not bijective.
390    /// Compatibility rules:
391    ///  * major and minor number must be the same
392    ///  * build and revision number of <paramref name="available"/> must be larger or equal to <paramref name="requested"/>.
393    /// </summary>
394    /// <param name="available">The available version which should be compared to <paramref name="requested"/>.</param>
395    /// <param name="requested">The requested version that must be matched.</param>
396    /// <returns></returns>
397    private static bool IsCompatiblePluginVersion(Version available, Version requested) {
398      // this condition must be removed after all plugins have been updated to declare plugin and dependency versions
399      if (
400        (requested.Major == 0 && requested.Minor == 0) ||
401        (available.Major == 0 && available.Minor == 0)) return true;
402      return
403        available.Major == requested.Major &&
404        available.Minor == requested.Minor &&
405        available.Build >= requested.Build &&
406        available.Revision >= requested.Revision;
407    }
408
409    private void CheckPluginDependencyCycles(IEnumerable<PluginDescription> pluginDescriptions) {
410      foreach (var plugin in pluginDescriptions) {
411        // if the plugin is not disabled check if there are cycles
412        if (plugin.PluginState != PluginState.Disabled && HasCycleInDependencies(plugin, plugin.Dependencies)) {
413          plugin.Disable("Dependency graph has a cycle.");
414        }
415      }
416    }
417
418    private bool HasCycleInDependencies(PluginDescription plugin, IEnumerable<PluginDescription> pluginDependencies) {
419      foreach (var dep in pluginDependencies) {
420        // if one of the dependencies is the original plugin we found a cycle and can return
421        // if the dependency is already disabled we can ignore the cycle detection because we will disable the plugin anyway
422        // if following one of the dependencies recursively leads to a cycle then we also return
423        if (dep == plugin || dep.PluginState == PluginState.Disabled || HasCycleInDependencies(plugin, dep.Dependencies)) return true;
424      }
425      // no cycle found and none of the direct and indirect dependencies is disabled
426      return false;
427    }
428
429    private void CheckPluginDependencies(IEnumerable<PluginDescription> pluginDescriptions) {
430      foreach (PluginDescription pluginDescription in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
431        List<PluginDescription> disabledPlugins = new List<PluginDescription>();
432        if (IsAnyDependencyDisabled(pluginDescription, disabledPlugins)) {
433          StringBuilder errorStrBuilder = new StringBuilder();
434          errorStrBuilder.AppendLine("Dependencies are disabled:");
435          foreach (var disabledPlugin in disabledPlugins) {
436            errorStrBuilder.AppendLine(disabledPlugin.Name + " " + disabledPlugin.Version);
437          }
438          pluginDescription.Disable(errorStrBuilder.ToString());
439        }
440      }
441    }
442
443    private bool IsAnyDependencyDisabled(PluginDescription descr, List<PluginDescription> disabledPlugins) {
444      if (descr.PluginState == PluginState.Disabled) {
445        disabledPlugins.Add(descr);
446        return true;
447      }
448      foreach (PluginDescription dependency in descr.Dependencies) {
449        IsAnyDependencyDisabled(dependency, disabledPlugins);
450      }
451      return disabledPlugins.Count > 0;
452    }
453  }
454  #endregion
455}
Note: See TracBrowser for help on using the repository browser.