Free cookie consent management tool by TermsFeed Policy Generator

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

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

Fixed caching and resolve event handler in plugin infrastructure to fix the bug that the SqlServerCompact assembly is not found when opening a db connection. #854 (System.Data.SqlServerCe assembly is not loaded correctly)

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