Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.PluginInfrastructure/Manager/PluginValidator.cs @ 2754

Last change on this file since 2754 was 2750, checked in by gkronber, 14 years ago

Prepared plugin infrastructure to work with versioned plugins and dependencies and added versioning to part of HL.GP plugins. #864 (Plugins should have an a version)

File size: 21.6 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2008 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
24using System.Text;
25using System.Reflection;
26using System.IO;
27using System.Diagnostics;
28using System.Linq;
29using System.Security;
30
31
32namespace HeuristicLab.PluginInfrastructure.Manager {
33  /// <summary>
34  /// Discovers all installed plugins in the plugin directory. Checks correctness of plugin meta-data and if
35  /// all plugin files are available and checks plugin dependencies.
36  /// </summary>
37  internal sealed class PluginValidator : MarshalByRefObject {
38    // private class to store plugin dependency declarations while reflecting over plugins
39    private class PluginDependency {
40      public string Name { get; private set; }
41      public Version Version { get; private set; }
42
43      public PluginDependency(string name, Version version) {
44        this.Name = name;
45        this.Version = version;
46      }
47    }
48
49
50    internal event EventHandler<PluginInfrastructureEventArgs> PluginLoaded;
51
52    private Dictionary<PluginDescription, IEnumerable<PluginDependency>> pluginDependencies;
53
54    private List<ApplicationDescription> applications;
55    internal IEnumerable<ApplicationDescription> Applications {
56      get {
57        if (string.IsNullOrEmpty(PluginDir)) throw new InvalidOperationException("PluginDir is not set.");
58        if (applications == null) DiscoverAndCheckPlugins();
59        return applications;
60      }
61    }
62
63    private IEnumerable<PluginDescription> plugins;
64    internal IEnumerable<PluginDescription> Plugins {
65      get {
66        if (string.IsNullOrEmpty(PluginDir)) throw new InvalidOperationException("PluginDir is not set.");
67        if (plugins == null) DiscoverAndCheckPlugins();
68        return plugins;
69      }
70    }
71
72    internal string PluginDir { get; set; }
73
74    internal PluginValidator() {
75      this.pluginDependencies = new Dictionary<PluginDescription, IEnumerable<PluginDependency>>();
76
77      // ReflectionOnlyAssemblyResolveEvent must be handled because we load assemblies from the plugin path
78      // (which is not listed in the default assembly lookup locations)
79      AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ReflectionOnlyAssemblyResolveEventHandler;
80    }
81
82    private Dictionary<string, Assembly> reflectionOnlyAssemblies = new Dictionary<string, Assembly>();
83    private Assembly ReflectionOnlyAssemblyResolveEventHandler(object sender, ResolveEventArgs args) {
84      if (reflectionOnlyAssemblies.ContainsKey(args.Name))
85        return reflectionOnlyAssemblies[args.Name];
86      else
87        return Assembly.ReflectionOnlyLoad(args.Name);
88    }
89
90
91    /// <summary>
92    /// Init first clears all internal datastructures (including plugin lists)
93    /// 1. All assemblies in the plugins directory are loaded into the reflection only context.
94    /// 2. The validator checks if all necessary files for each plugin are available.
95    /// 3. The validator checks if all declared plugin assemblies can be loaded.
96    /// 4. The validator builds the tree of plugin descriptions (dependencies)
97    /// 5. The validator checks if there are any cycles in the plugin dependency graph and disables plugin with circular dependencies
98    /// 6. The validator checks for each plugin if any dependency is disabled.
99    /// 7. All plugins that are not disabled are loaded into the execution context.
100    /// 8. Each loaded plugin (all assemblies) is searched for a types that implement IPlugin
101    ///    then one instance of each IPlugin type is activated and the OnLoad hook is called.
102    /// 9. All types implementing IApplication are discovered
103    /// </summary>
104    internal void DiscoverAndCheckPlugins() {
105      pluginDependencies.Clear();
106
107      IEnumerable<Assembly> reflectionOnlyAssemblies = ReflectionOnlyLoadDlls(PluginDir);
108      IEnumerable<PluginDescription> pluginDescriptions = GatherPluginDescriptions(reflectionOnlyAssemblies);
109      CheckPluginFiles(pluginDescriptions);
110
111      // check if all plugin assemblies can be loaded
112      CheckPluginAssemblies(pluginDescriptions);
113
114      // a full list of plugin descriptions is available now we can build the dependency tree
115      BuildDependencyTree(pluginDescriptions);
116
117      // check for dependency cycles
118      CheckPluginDependencyCycles(pluginDescriptions);
119
120      // recursively check if all necessary plugins are available and not disabled
121      // disable plugins with missing or disabled dependencies
122      CheckPluginDependencies(pluginDescriptions);
123
124      // mark all plugins as enabled that were not disabled in CheckPluginFiles, CheckPluginAssemblies,
125      // CheckCircularDependencies, or CheckPluginDependencies
126      foreach (var desc in pluginDescriptions)
127        if (desc.PluginState != PluginState.Disabled)
128          desc.Enable();
129
130      // test full loading (in contrast to reflection only loading) of plugins
131      // disables plugins that are not loaded correctly
132      LoadPlugins(pluginDescriptions);
133
134      plugins = pluginDescriptions;
135      DiscoverApplications();
136    }
137
138    private void DiscoverApplications() {
139      applications = new List<ApplicationDescription>();
140
141      foreach (IApplication application in GetApplications()) {
142        Type appType = application.GetType();
143        ApplicationAttribute attr = (from x in appType.GetCustomAttributes(typeof(ApplicationAttribute), false)
144                                     select (ApplicationAttribute)x).Single();
145        ApplicationDescription info = new ApplicationDescription();
146        info.Name = application.Name;
147        info.Version = appType.Assembly.GetName().Version;
148        info.Description = application.Description;
149        info.AutoRestart = attr.RestartOnErrors;
150        info.DeclaringAssemblyName = appType.Assembly.GetName().Name;
151        info.DeclaringTypeName = appType.Namespace + "." + application.GetType().Name;
152
153        applications.Add(info);
154      }
155    }
156
157    private static IEnumerable<IApplication> GetApplications() {
158      return from asm in AppDomain.CurrentDomain.GetAssemblies()
159             from t in asm.GetTypes()
160             where typeof(IApplication).IsAssignableFrom(t) &&
161               !t.IsAbstract && !t.IsInterface && !t.HasElementType
162             select (IApplication)Activator.CreateInstance(t);
163    }
164
165    private IEnumerable<Assembly> ReflectionOnlyLoadDlls(string baseDir) {
166      List<Assembly> assemblies = new List<Assembly>();
167      // recursively load .dll files in subdirectories
168      foreach (string dirName in Directory.GetDirectories(baseDir)) {
169        assemblies.AddRange(ReflectionOnlyLoadDlls(dirName));
170      }
171      // try to load each .dll file in the plugin directory into the reflection only context
172      foreach (string filename in Directory.GetFiles(baseDir, "*.dll")) {
173        try {
174          Assembly asm = Assembly.ReflectionOnlyLoadFrom(filename);
175          RegisterLoadedAssembly(asm);
176          assemblies.Add(asm);
177        }
178        catch (BadImageFormatException) { } // just ignore the case that the .dll file is not a CLR assembly (e.g. a native dll)
179        catch (FileLoadException) { }
180        catch (SecurityException) { }
181      }
182      return assemblies;
183    }
184
185    /// <summary>
186    /// Checks if all plugin assemblies can be loaded. If an assembly can't be loaded the plugin is disabled.
187    /// </summary>
188    /// <param name="pluginDescriptions"></param>
189    private void CheckPluginAssemblies(IEnumerable<PluginDescription> pluginDescriptions) {
190      foreach (var desc in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
191        try {
192          foreach (var asmLocation in desc.AssemblyLocations) {
193            // the assembly must have been loaded in ReflectionOnlyDlls
194            // so we simply determine the name of the assembly and try to find it in the cache of loaded assemblies
195            var asmName = AssemblyName.GetAssemblyName(asmLocation);
196
197            if (!reflectionOnlyAssemblies.ContainsKey(asmName.FullName)) {
198              desc.Disable();
199              break; // as soon as one assembly is not available disable the plugin and check the next plugin description
200            }
201          }
202        }
203        catch (BadImageFormatException) {
204          // disable the plugin
205          desc.Disable();
206        }
207        catch (FileNotFoundException) {
208          // disable the plugin
209          desc.Disable();
210        }
211        catch (FileLoadException) {
212          // disable the plugin
213          desc.Disable();
214        }
215        catch (ArgumentException) {
216          // disable the plugin
217          desc.Disable();
218        }
219        catch (SecurityException) {
220          // disable the plugin
221          desc.Disable();
222        }
223      }
224    }
225
226
227    // find all types implementing IPlugin in the reflectionOnlyAssemblies and create a list of plugin descriptions
228    // the dependencies in the plugin descriptions are not yet set correctly because we need to create
229    // the full list of all plugin descriptions first
230    private IEnumerable<PluginDescription> GatherPluginDescriptions(IEnumerable<Assembly> assemblies) {
231      List<PluginDescription> pluginDescriptions = new List<PluginDescription>();
232      foreach (Assembly assembly in assemblies) {
233        // GetExportedTypes throws FileNotFoundException when a referenced assembly
234        // of the current assembly is missing.
235        try {
236          // if there is a type that implements IPlugin
237          // use AssemblyQualifiedName to compare the types because we can't directly
238          // compare ReflectionOnly types and execution types
239          var assemblyPluginDescriptions = from t in assembly.GetExportedTypes()
240                                           where !t.IsAbstract && t.GetInterfaces().Any(x => x.AssemblyQualifiedName == typeof(IPlugin).AssemblyQualifiedName)
241                                           select GetPluginDescription(t);
242          pluginDescriptions.AddRange(assemblyPluginDescriptions);
243        }
244        // ignore exceptions. Just don't yield a plugin description when an exception is thrown
245        catch (FileNotFoundException) {
246        }
247        catch (FileLoadException) {
248        }
249        catch (InvalidPluginException) {
250        }
251      }
252      return pluginDescriptions;
253    }
254
255    /// <summary>
256    /// Extracts plugin information for this type.
257    /// Reads plugin name, list and type of files and dependencies of the plugin. This information is necessary for
258    /// plugin dependency checking before plugin activation.
259    /// </summary>
260    /// <param name="t"></param>
261    private PluginDescription GetPluginDescription(Type pluginType) {
262      // get all attributes of that type
263      IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(pluginType);
264      List<PluginDependency> pluginDependencies = new List<PluginDependency>();
265      List<PluginFile> pluginFiles = new List<PluginFile>();
266      string pluginName = null;
267      string pluginDescription = null;
268      string pluginVersion = "0.0.0.0";
269      // iterate through all custom attributes and search for attributed that we are interested in
270      foreach (CustomAttributeData attributeData in attributes) {
271        if (IsAttributeDataForType(attributeData, typeof(PluginAttribute))) {
272          pluginName = (string)attributeData.ConstructorArguments[0].Value;
273          if (attributeData.ConstructorArguments.Count() > 1) {
274            pluginVersion = (string)attributeData.ConstructorArguments[1].Value;
275          }
276          if (attributeData.ConstructorArguments.Count() > 2) {
277            pluginDescription = (string)attributeData.ConstructorArguments[2].Value;
278          } else {
279            // no description given => use name as description
280            pluginDescription = pluginName;
281          }
282        } else if (IsAttributeDataForType(attributeData, typeof(PluginDependencyAttribute))) {
283          string name = (string)attributeData.ConstructorArguments[0].Value;
284          Version version = new Version();
285          // check if version is given for now
286          // later when the constructore of PluginDependencyAttribute with only one argument has been removed
287          // this conditional can be removed as well
288          if (attributeData.ConstructorArguments.Count > 1) {
289            try {
290              version = new Version((string)attributeData.ConstructorArguments[1].Value); // throws FormatException
291            }
292            catch (FormatException ex) {
293              throw new InvalidPluginException("Invalid version format of dependency " + name + " in plugin " + pluginType.ToString(), ex);
294            }
295          }
296          pluginDependencies.Add(new PluginDependency(name, version));
297        } else if (IsAttributeDataForType(attributeData, typeof(PluginFileAttribute))) {
298          string pluginFileName = (string)attributeData.ConstructorArguments[0].Value;
299          PluginFileType fileType = (PluginFileType)attributeData.ConstructorArguments[1].Value;
300          pluginFiles.Add(new PluginFile(Path.GetFullPath(Path.Combine(PluginDir, pluginFileName)), fileType));
301        }
302      }
303
304      // minimal sanity check of the attribute values
305      if (!string.IsNullOrEmpty(pluginName) &&
306          pluginFiles.Count > 0 &&                                   // at least on file
307          pluginFiles.Any(f => f.Type == PluginFileType.Assembly)) { // at least on assembly
308        // create a temporary PluginDescription that contains the attribute values
309        PluginDescription info = new PluginDescription();
310        info.Name = pluginName;
311        info.Description = pluginDescription;
312        info.Version = new Version(pluginVersion);
313        info.AddFiles(pluginFiles);
314
315        this.pluginDependencies[info] = pluginDependencies;
316        return info;
317      } else {
318        throw new InvalidPluginException("Invalid metadata in plugin " + pluginType.ToString());
319      }
320    }
321
322    private static bool IsAttributeDataForType(CustomAttributeData attributeData, Type attributeType) {
323      return attributeData.Constructor.DeclaringType.AssemblyQualifiedName == attributeType.AssemblyQualifiedName;
324    }
325
326    // builds a dependency tree of all plugin descriptions
327    // searches matching plugin descriptions based on the list of dependency names for each plugin
328    // and sets the dependencies in the plugin descriptions
329    private void BuildDependencyTree(IEnumerable<PluginDescription> pluginDescriptions) {
330      foreach (var desc in pluginDescriptions) {
331        foreach (var dependency in pluginDependencies[desc]) {
332          var matchingDescriptions = from availablePlugin in pluginDescriptions
333                                     where availablePlugin.Name == dependency.Name
334                                     where IsCompatiblePluginVersion(availablePlugin.Version, dependency.Version)
335                                     select availablePlugin;
336          if (matchingDescriptions.Count() > 0) {
337            desc.AddDependency(matchingDescriptions.Single());
338          } else {
339            // no plugin description that matches the dependency name is available => plugin is disabled
340            desc.Disable(); break;
341          }
342        }
343      }
344    }
345
346    /// <summary>
347    /// Checks if version <paramref name="available"/> is compatible to version <paramref name="requested"/>.
348    /// Note: the compatibility relation is not bijective.
349    /// Compatibility rules:
350    ///  * major and minor number must be the same
351    ///  * build and revision number of <paramref name="available"/> must be larger or equal to <paramref name="requested"/>.
352    /// </summary>
353    /// <param name="available">The available version which should be compared to <paramref name="requested"/>.</param>
354    /// <param name="requested">The requested version that must be matched.</param>
355    /// <returns></returns>
356    private bool IsCompatiblePluginVersion(Version available, Version requested) {
357      // this condition must be removed after all plugins have been updated to declare plugin and dependency versions
358      if (
359        (requested.Major == 0 && requested.Minor == 0) ||
360        (available.Major == 0 && available.Minor == 0)) return true;
361      return
362        available.Major == requested.Major &&
363        available.Minor == requested.Minor &&
364        available.Build >= requested.Build &&
365        available.Revision >= requested.Revision;
366    }
367
368    private void CheckPluginDependencyCycles(IEnumerable<PluginDescription> pluginDescriptions) {
369      foreach (var plugin in pluginDescriptions) {
370        // if the plugin is not disabled anyway check if there are cycles
371        if (plugin.PluginState != PluginState.Disabled && HasCycleInDependencies(plugin, plugin.Dependencies)) {
372          plugin.Disable();
373        }
374      }
375    }
376
377    private bool HasCycleInDependencies(PluginDescription plugin, IEnumerable<PluginDescription> pluginDependencies) {
378      foreach (var dep in pluginDependencies) {
379        // if one of the dependencies is the original plugin we found a cycle and can return
380        // if the dependency is already disabled we can ignore the cycle detection because we will disable the plugin anyway
381        // if following one of the dependencies recursively leads to a cycle then we also return
382        if (dep == plugin || dep.PluginState == PluginState.Disabled || HasCycleInDependencies(plugin, dep.Dependencies)) return true;
383      }
384      // no cycle found and none of the direct and indirect dependencies is disabled
385      return false;
386    }
387
388    private void CheckPluginDependencies(IEnumerable<PluginDescription> pluginDescriptions) {
389      foreach (PluginDescription pluginDescription in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
390        if (IsAnyDependencyDisabled(pluginDescription)) {
391          pluginDescription.Disable();
392        }
393      }
394    }
395
396
397    private bool IsAnyDependencyDisabled(PluginDescription descr) {
398      if (descr.PluginState == PluginState.Disabled) return true;
399      foreach (PluginDescription dependency in descr.Dependencies) {
400        if (IsAnyDependencyDisabled(dependency)) return true;
401      }
402      return false;
403    }
404
405    private void LoadPlugins(IEnumerable<PluginDescription> pluginDescriptions) {
406      // load all loadable plugins (all dependencies available) into the execution context
407      foreach (var desc in PluginDescriptionIterator.IterateDependenciesBottomUp(pluginDescriptions
408                                                                                .Where(x => x.PluginState != PluginState.Disabled))) {
409        List<Type> types = new List<Type>();
410        foreach (string assemblyLocation in desc.AssemblyLocations) {
411          // now load the assemblies into the execution context
412          var asm = Assembly.LoadFrom(assemblyLocation);
413          foreach (Type t in asm.GetTypes()) {
414            if (typeof(IPlugin).IsAssignableFrom(t)) {
415              types.Add(t);
416            }
417          }
418        }
419
420        foreach (Type pluginType in types) {
421          if (!pluginType.IsAbstract && !pluginType.IsInterface && !pluginType.HasElementType) {
422            IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType);
423            plugin.OnLoad();
424            OnPluginLoaded(new PluginInfrastructureEventArgs("Plugin loaded", plugin.Name));
425          }
426        }
427        desc.Load();
428      }
429    }
430
431    // checks if all declared plugin files are actually available and disables plugins with missing files
432    private void CheckPluginFiles(IEnumerable<PluginDescription> pluginDescriptions) {
433      foreach (PluginDescription desc in pluginDescriptions) {
434        if (!CheckPluginFiles(desc)) {
435          desc.Disable();
436        }
437      }
438    }
439
440    private bool CheckPluginFiles(PluginDescription pluginDescription) {
441      foreach (string filename in pluginDescription.Files.Select(x => x.Name)) {
442        if (!FileLiesInDirectory(PluginDir, filename) ||
443          !File.Exists(filename)) {
444          return false;
445        }
446      }
447      return true;
448    }
449
450    private static bool FileLiesInDirectory(string dir, string fileName) {
451      var basePath = Path.GetFullPath(dir);
452      return Path.GetFullPath(fileName).StartsWith(basePath);
453    }
454
455    // register assembly in the assembly cache for the ReflectionOnlyAssemblyResolveEvent
456    private void RegisterLoadedAssembly(Assembly asm) {
457      reflectionOnlyAssemblies.Add(asm.FullName, asm);
458      reflectionOnlyAssemblies.Add(asm.GetName().Name, asm); // add short name
459    }
460
461    internal void OnPluginLoaded(PluginInfrastructureEventArgs e) {
462      if (PluginLoaded != null)
463        PluginLoaded(this, e);
464    }
465
466    /// <summary>
467    /// Initializes the life time service with an infinite lease time.
468    /// </summary>
469    /// <returns><c>null</c>.</returns>
470    public override object InitializeLifetimeService() {
471      return null;
472    }
473  }
474}
Note: See TracBrowser for help on using the repository browser.