Free cookie consent management tool by TermsFeed Policy Generator

source: branches/PluginInfrastructure Refactoring/HeuristicLab.PluginInfrastructure/Manager/PluginValidator.cs @ 2527

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

Implemented changes as requested by swagner. #799

File size: 16.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    internal event EventHandler<PluginInfrastructureEventArgs> PluginLoaded;
39
40    private Dictionary<PluginDescription, List<string>> pluginDependencies;
41
42    private List<ApplicationDescription> applications;
43    internal IEnumerable<ApplicationDescription> Applications {
44      get {
45        if (string.IsNullOrEmpty(PluginDir)) throw new InvalidOperationException("PluginDir is not set.");
46        if (applications == null) DiscoverAndCheckPlugins();
47        return applications;
48      }
49    }
50
51    private IEnumerable<PluginDescription> plugins;
52    internal IEnumerable<PluginDescription> Plugins {
53      get {
54        if (string.IsNullOrEmpty(PluginDir)) throw new InvalidOperationException("PluginDir is not set.");
55        if (plugins == null) DiscoverAndCheckPlugins();
56        return plugins;
57      }
58    }
59
60    internal string PluginDir { get; set; }
61
62    internal PluginValidator() {
63      this.pluginDependencies = new Dictionary<PluginDescription, List<string>>();
64
65      // ReflectionOnlyAssemblyResolveEvent must be handled because we load assemblies from the plugin path
66      // (which is not listed in the default assembly lookup locations)
67      AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ReflectionOnlyAssemblyResolveEventHandler;
68    }
69
70    private Assembly ReflectionOnlyAssemblyResolveEventHandler(object sender, ResolveEventArgs args) {
71      return Assembly.ReflectionOnlyLoad(args.Name);
72    }
73
74
75    /// <summary>
76    /// Init first clears all internal datastructures (including plugin lists)
77    /// 1. All assemblies in the plugins directory are loaded into the reflection only context.
78    /// 2. The validator checks if all necessary files for each plugin are available.
79    /// 3. The validator builds the tree of plugin descriptions (dependencies)
80    /// 4. The validator checks if all dependencies for each plugin are ok.
81    /// 5. All plugins for which there are no dependencies missing are loaded into the execution context.
82    /// 6. Each loaded plugin (all assemblies) is searched for a types that implement IPlugin
83    ///    then one instance of each IPlugin type is activated and the OnLoad hook is called.
84    /// 7. All types implementing IApplication are discovered
85    /// </summary>
86    internal void DiscoverAndCheckPlugins() {
87      pluginDependencies.Clear();
88
89      IEnumerable<Assembly> reflectionOnlyAssemblies = ReflectionOnlyLoadDlls(PluginDir);
90      IEnumerable<PluginDescription> pluginDescriptions = GatherPluginDescriptions(reflectionOnlyAssemblies);
91      CheckPluginFiles(pluginDescriptions);
92
93      CheckPluginAssemblies(pluginDescriptions);
94
95      // a full list of plugin descriptions is available now we can build the dependency tree
96      BuildDependencyTree(pluginDescriptions);
97
98      // recursively check if all necessary plugins are available and not disabled
99      // disable plugins with missing or disabled dependencies
100      CheckPluginDependencies(pluginDescriptions);
101
102      // mark all plugins as enabled that were not disabled in CheckPluginFiles or CheckPluginDependencies
103      foreach (var desc in pluginDescriptions)
104        if (desc.PluginState != PluginState.Disabled)
105          desc.Enable();
106
107      // test full loading (in contrast to reflection only loading) of plugins
108      // disables plugins that are not loaded correctly
109      LoadPlugins(pluginDescriptions);
110
111      plugins = pluginDescriptions;
112      DiscoverApplications();
113    }
114
115    private void DiscoverApplications() {
116      applications = new List<ApplicationDescription>();
117
118      foreach (IApplication application in GetApplications()) {
119        Type appType = application.GetType();
120        ApplicationAttribute attr = (from x in appType.GetCustomAttributes(typeof(ApplicationAttribute), false)
121                                     select (ApplicationAttribute)x).Single();
122        ApplicationDescription info = new ApplicationDescription();
123        info.Name = application.Name;
124        info.Version = appType.Assembly.GetName().Version;
125        info.Description = application.Description;
126        info.AutoRestart = attr.RestartOnErrors;
127        info.DeclaringAssemblyName = appType.Assembly.GetName().Name;
128        info.DeclaringTypeName = appType.Namespace + "." + application.GetType().Name;
129
130        applications.Add(info);
131      }
132    }
133
134    private static IEnumerable<IApplication> GetApplications() {
135      return from asm in AppDomain.CurrentDomain.GetAssemblies()
136             from t in asm.GetTypes()
137             where typeof(IApplication).IsAssignableFrom(t) &&
138               !t.IsAbstract && !t.IsInterface && !t.HasElementType
139             select (IApplication)Activator.CreateInstance(t);
140    }
141
142    private static IEnumerable<Assembly> ReflectionOnlyLoadDlls(string baseDir) {
143      List<Assembly> assemblies = new List<Assembly>();
144      // recursively load .dll files in subdirectories
145      foreach (string dirName in Directory.GetDirectories(baseDir)) {
146        assemblies.AddRange(ReflectionOnlyLoadDlls(dirName));
147      }
148      // try to load each .dll file in the plugin directory into the reflection only context
149      foreach (string filename in Directory.GetFiles(baseDir, "*.dll")) {
150        try {
151          assemblies.Add(Assembly.ReflectionOnlyLoadFrom(filename));
152        }
153        catch (BadImageFormatException) { } // just ignore the case that the .dll file is not a CLR assembly (e.g. a native dll)
154        catch (FileLoadException) { }
155        catch (SecurityException) { }
156      }
157      return assemblies;
158    }
159
160    /// <summary>
161    /// Checks if all plugin assemblies can be loaded. If an assembly can't be loaded the plugin is disabled.
162    /// </summary>
163    /// <param name="pluginDescriptions"></param>
164    private void CheckPluginAssemblies(IEnumerable<PluginDescription> pluginDescriptions) {
165      foreach (var desc in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
166        try {
167          foreach (var asm in desc.Assemblies) {
168            Assembly.ReflectionOnlyLoadFrom(asm);
169          }
170        }
171        catch (BadImageFormatException) {
172          // disable the plugin
173          desc.Disable();
174        }
175        catch (FileNotFoundException) {
176          // disable the plugin
177          desc.Disable();
178        }
179        catch (FileLoadException) {
180          // disable the plugin
181          desc.Disable();
182        }
183        catch (ArgumentException) {
184          // disable the plugin
185          desc.Disable();
186        }
187        catch (SecurityException) {
188          // disable the plugin
189          desc.Disable();
190        }
191      }
192    }
193
194
195    // find all types implementing IPlugin in the reflectionOnlyAssemblies and create a list of plugin descriptions
196    // the dependencies in the plugin descriptions are not yet set correctly because we need to create
197    // the full list of all plugin descriptions first
198    private IEnumerable<PluginDescription> GatherPluginDescriptions(IEnumerable<Assembly> assemblies) {
199      List<PluginDescription> pluginDescriptions = new List<PluginDescription>();
200      foreach (Assembly assembly in assemblies) {
201        // GetExportedTypes throws FileNotFoundException when a referenced assembly
202        // of the current assembly is missing.
203        try {
204          // if there is a type that implements IPlugin
205          // use AssemblyQualifiedName to compare the types because we can't directly
206          // compare ReflectionOnly types and execution types
207          var assemblyPluginDescriptions = from t in assembly.GetExportedTypes()
208                                           where !t.IsAbstract && t.GetInterfaces().Any(x => x.AssemblyQualifiedName == typeof(IPlugin).AssemblyQualifiedName)
209                                           select GetPluginDescription(t);
210          pluginDescriptions.AddRange(assemblyPluginDescriptions);
211        }
212        // ignore exceptions. Just don't yield a plugin description when an exception is thrown
213        catch (FileNotFoundException) {
214        }
215        catch (FileLoadException) {
216        }
217        catch (InvalidPluginException) {
218        }
219      }
220      return pluginDescriptions;
221    }
222
223    /// <summary>
224    /// Extracts plugin information for this type.
225    /// Reads plugin name, list and type of files and dependencies of the plugin. This information is necessary for
226    /// plugin dependency checking before plugin activation.
227    /// </summary>
228    /// <param name="t"></param>
229    private PluginDescription GetPluginDescription(Type pluginType) {
230      // get all attributes of that type
231      IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(pluginType);
232      List<string> pluginAssemblies = new List<string>();
233      List<string> pluginDependencies = new List<string>();
234      List<string> pluginFiles = new List<string>();
235      string pluginName = null;
236      string pluginDescription = null;
237      // iterate through all custom attributes and search for attributed that we are interested in
238      foreach (CustomAttributeData attributeData in attributes) {
239        if (IsAttributeDataForType(attributeData, typeof(PluginAttribute))) {
240          pluginName = (string)attributeData.ConstructorArguments[0].Value;
241          if (attributeData.ConstructorArguments.Count() == 2) {
242            pluginDescription = (string)attributeData.ConstructorArguments[1].Value;
243          } else pluginDescription = pluginName;
244        } else if (IsAttributeDataForType(attributeData, typeof(PluginDependencyAttribute))) {
245          pluginDependencies.Add((string)attributeData.ConstructorArguments[0].Value);
246        } else if (IsAttributeDataForType(attributeData, typeof(PluginFileAttribute))) {
247          string pluginFileName = (string)attributeData.ConstructorArguments[0].Value;
248          PluginFileType fileType = (PluginFileType)attributeData.ConstructorArguments[1].Value;
249          pluginFiles.Add(Path.GetFullPath(Path.Combine(PluginDir, pluginFileName)));
250          if (fileType == PluginFileType.Assembly) {
251            pluginAssemblies.Add(Path.GetFullPath(Path.Combine(PluginDir, pluginFileName)));
252          }
253        }
254      }
255
256      var buildDates = from attr in CustomAttributeData.GetCustomAttributes(pluginType.Assembly)
257                       where IsAttributeDataForType(attr, typeof(AssemblyBuildDateAttribute))
258                       select (string)attr.ConstructorArguments[0].Value;
259
260      // minimal sanity check of the attribute values
261      if (!string.IsNullOrEmpty(pluginName) &&
262          pluginFiles.Count > 0 &&
263          pluginAssemblies.Count > 0 &&
264          buildDates.Count() == 1) {
265        // create a temporary PluginDescription that contains the attribute values
266        PluginDescription info = new PluginDescription();
267        info.Name = pluginName;
268        info.Description = pluginDescription;
269        info.Version = pluginType.Assembly.GetName().Version;
270        info.BuildDate = DateTime.Parse(buildDates.Single(), System.Globalization.CultureInfo.InvariantCulture);
271        info.AddAssemblies(pluginAssemblies);
272        info.AddFiles(pluginFiles);
273
274        this.pluginDependencies[info] = pluginDependencies;
275        return info;
276      } else {
277        throw new InvalidPluginException("Invalid metadata in plugin " + pluginType.ToString());
278      }
279    }
280
281    private static bool IsAttributeDataForType(CustomAttributeData attributeData, Type attributeType) {
282      return attributeData.Constructor.DeclaringType.AssemblyQualifiedName == attributeType.AssemblyQualifiedName;
283    }
284
285    // builds a dependency tree of all plugin descriptions
286    // searches matching plugin descriptions based on the list of dependency names for each plugin
287    // and sets the dependencies in the plugin descriptions
288    private void BuildDependencyTree(IEnumerable<PluginDescription> pluginDescriptions) {
289      foreach (var desc in pluginDescriptions) {
290        foreach (string pluginName in pluginDependencies[desc]) {
291          var matchingDescriptions = pluginDescriptions.Where(x => x.Name == pluginName);
292          if (matchingDescriptions.Count() > 0) {
293            desc.AddDependency(matchingDescriptions.Single());
294          } else {
295            // no plugin description that matches the dependency name is available => plugin is disabled
296            desc.Disable();
297          }
298        }
299      }
300    }
301
302    private void CheckPluginDependencies(IEnumerable<PluginDescription> pluginDescriptions) {
303      foreach (PluginDescription pluginDescription in pluginDescriptions.Where(x => x.PluginState != PluginState.Disabled)) {
304        if (IsAnyDependencyDisabled(pluginDescription)) {
305          pluginDescription.Disable();
306        }
307      }
308    }
309
310
311    private bool IsAnyDependencyDisabled(PluginDescription descr) {
312      if (descr.PluginState == PluginState.Disabled) return true;
313      foreach (PluginDescription dependency in descr.Dependencies) {
314        if (IsAnyDependencyDisabled(dependency)) return true;
315      }
316      return false;
317    }
318
319    private void LoadPlugins(IEnumerable<PluginDescription> pluginDescriptions) {
320      // load all loadable plugins (all dependencies available) into the execution context
321      foreach (var desc in PluginDescriptionIterator.IterateDependenciesBottomUp(pluginDescriptions
322                                                                                .Where(x => x.PluginState != PluginState.Disabled))) {
323        List<Type> types = new List<Type>();
324        foreach (string assembly in desc.Assemblies) {
325          var asm = Assembly.LoadFrom(assembly);
326          foreach (Type t in asm.GetTypes()) {
327            if (typeof(IPlugin).IsAssignableFrom(t)) {
328              types.Add(t);
329            }
330          }
331        }
332
333        foreach (Type pluginType in types) {
334          if (!pluginType.IsAbstract && !pluginType.IsInterface && !pluginType.HasElementType) {
335            IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType);
336            plugin.OnLoad();
337            OnPluginLoaded(new PluginInfrastructureEventArgs("Plugin loaded", plugin.Name));
338          }
339        }
340        desc.Load();
341      }
342    }
343
344    // checks if all declared plugin files are actually available and disables plugins with missing files
345    private void CheckPluginFiles(IEnumerable<PluginDescription> pluginDescriptions) {
346      foreach (PluginDescription desc in pluginDescriptions) {
347        if (!CheckPluginFiles(desc)) {
348          desc.Disable();
349        }
350      }
351    }
352
353    private bool CheckPluginFiles(PluginDescription pluginDescription) {
354      foreach (string filename in pluginDescription.Files) {
355        if (!FileLiesInDirectory(PluginDir, filename) ||
356          !File.Exists(filename)) {
357          return false;
358        }
359      }
360      return true;
361    }
362
363    private static bool FileLiesInDirectory(string dir, string fileName) {
364      var basePath = Path.GetFullPath(dir);
365      return Path.GetFullPath(fileName).StartsWith(basePath);
366    }
367
368    internal void OnPluginLoaded(PluginInfrastructureEventArgs e) {
369      if (PluginLoaded != null)
370        PluginLoaded(this, e);
371    }
372
373    /// <summary>
374    /// Initializes the life time service with an infinite lease time.
375    /// </summary>
376    /// <returns><c>null</c>.</returns>
377    public override object InitializeLifetimeService() {
378      return null;
379    }
380  }
381}
Note: See TracBrowser for help on using the repository browser.