source: trunk/eraser/Eraser.Manager/Plugins.cs @ 2056

Revision 2056, 17.9 KB checked in by lowjoel, 4 years ago (diff)

Undo some changes in r2050 where the assembly resolution code would be removed upon loading of plugins -- this caused certain types to be unable to load and crash the program.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008-2010 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By:
6 *
7 * This file is part of Eraser.
8 *
9 * Eraser is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later
12 * version.
13 *
14 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 *
18 * A copy of the GNU General Public License can be found at
19 * <http://www.gnu.org/licenses/>.
20 */
21
22using System;
23using System.Collections.Generic;
24using System.Text;
25using System.Linq;
26
27using System.IO;
28using System.Reflection;
29using System.Windows.Forms;
30using System.Runtime.InteropServices;
31using System.Security.Cryptography;
32using System.Security.Cryptography.X509Certificates;
33using Eraser.Util;
34
35namespace Eraser.Manager.Plugin
36{
37    /// <summary>
38    /// The plugins host interface which is used for communicating with the host
39    /// program.
40    /// </summary>
41    /// <remarks>Remember to call Load to load the plugins into memory, otherwise
42    /// they will never be loaded.</remarks>
43    public abstract class Host : IDisposable
44    {
45        #region IDisposable members
46        protected virtual void Dispose(bool disposing)
47        {
48        }
49
50        /// <summary>
51        /// Cleans up resources used by the host. Also unloads all loaded plugins.
52        /// </summary>
53        public void Dispose()
54        {
55            Dispose(true);
56            GC.SuppressFinalize(this);
57        }
58        #endregion
59
60        /// <summary>
61        /// Getter that retrieves the global plugin host instance.
62        /// </summary>
63        public static Host Instance
64        {
65            get { return ManagerLibrary.Instance.Host; }
66        }
67
68        /// <summary>
69        /// Retrieves the list of currently loaded plugins.
70        /// </summary>
71        /// <remarks>The returned list is read-only</remarks>
72        public abstract IList<PluginInstance> Plugins
73        {
74            get;
75        }
76
77        /// <summary>
78        /// Loads all plugins into memory.
79        /// </summary>
80        public abstract void Load();
81
82        /// <summary>
83        /// The plugin loaded event.
84        /// </summary>
85        public EventHandler<PluginLoadedEventArgs> PluginLoaded { get; set; }
86
87        /// <summary>
88        /// Event callback executor for the OnPluginLoad Event
89        /// </summary>
90        internal void OnPluginLoaded(object sender, PluginLoadedEventArgs e)
91        {
92            if (PluginLoaded != null)
93                PluginLoaded(sender, e);
94        }
95
96        /// <summary>
97        /// Loads a plugin.
98        /// </summary>
99        /// <param name="filePath">The absolute or relative file path to the
100        /// DLL.</param>
101        /// <returns>True if the plugin is loaded, false otherwise.</returns>
102        /// <remarks>If a plugin is loaded twice, this function should do nothing
103        /// and return True.</remarks>
104        public abstract bool LoadPlugin(string filePath);
105    }
106
107    /// <summary>
108    /// Event argument for the plugin loaded event.
109    /// </summary>
110    public class PluginLoadedEventArgs : EventArgs
111    {
112        /// <summary>
113        /// Constructor.
114        /// </summary>
115        /// <param name="instance">The plugin instance of the recently loaded plugin.</param>
116        public PluginLoadedEventArgs(PluginInstance instance)
117        {
118            Instance = instance;
119        }
120
121        /// <summary>
122        /// The <see cref="PluginInstance"/> object representing the newly loaded plugin.
123        /// </summary>
124        public PluginInstance Instance { get; private set; }
125    }
126
127    /// <summary>
128    /// The default plugins host implementation.
129    /// </summary>
130    internal class DefaultHost : Host
131    {
132        /// <summary>
133        /// Constructor. Loads all plugins in the Plugins folder.
134        /// </summary>
135        public DefaultHost()
136        {
137        }
138
139        public override void Load()
140        {
141            //Specify additional places to load assemblies from
142            AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
143            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ResolveReflectionDependency;
144
145            //Load all core plugins first
146            foreach (KeyValuePair<string, string> plugin in CorePlugins)
147            {
148                LoadCorePlugin(Path.Combine(PluginsFolder, plugin.Key), plugin.Value);
149            }
150
151            //Then load the rest
152            foreach (string fileName in Directory.GetFiles(PluginsFolder))
153            {
154                FileInfo file = new FileInfo(fileName);
155                if (file.Extension.Equals(".dll"))
156                    try
157                    {
158                        LoadPlugin(file.FullName);
159                    }
160                    catch (BadImageFormatException)
161                    {
162                    }
163                    catch (FileLoadException)
164                    {
165                    }
166            }
167        }
168
169        protected override void Dispose(bool disposing)
170        {
171            if (plugins == null)
172                return;
173
174            if (disposing)
175            {
176                //Unload all the plugins. This will cause all the plugins to execute
177                //the cleanup code.
178                foreach (PluginInstance plugin in plugins)
179                    if (plugin.Plugin != null)
180                        plugin.Plugin.Dispose();
181            }
182
183            plugins = null;
184        }
185
186        /// <summary>
187        /// The path to the folder containing the plugins.
188        /// </summary>
189        public readonly string PluginsFolder = Path.Combine(
190            Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), //Assembly location
191            "Plugins" //Plugins folder
192        );
193
194        /// <summary>
195        /// The list of plugins which are core, the key is the file name, the value
196        /// is the assembly name.
197        /// </summary>
198        private readonly KeyValuePair<string, string>[] CorePlugins =
199            new KeyValuePair<string, string>[]
200            {
201                new KeyValuePair<string, string>(
202                    "Eraser.DefaultPlugins.dll",
203                    "Eraser.DefaultPlugins"
204                )
205            };
206
207        public override IList<PluginInstance> Plugins
208        {
209            get { return plugins.AsReadOnly(); }
210        }
211
212        /// <summary>
213        /// Verifies whether the provided assembly is a plugin.
214        /// </summary>
215        /// <param name="assembly">The assembly to verify.</param>
216        /// <returns>True if the assembly provided is a plugin, false otherwise.</returns>
217        private bool IsPlugin(Assembly assembly)
218        {
219            //Iterate over every exported type, checking if it implements IPlugin
220            Type typePlugin = assembly.GetExportedTypes().FirstOrDefault(
221                    type => type.GetInterface("Eraser.Manager.Plugin.IPlugin", true) != null);
222
223            //If the typePlugin type is empty the assembly doesn't implement IPlugin it's not
224            //a plugin.
225            return typePlugin != null;
226        }
227
228        /// <summary>
229        /// Loads the assembly at the specified path, and verifying its assembly name,
230        /// ensuring that the assembly contains a core plugin.
231        /// </summary>
232        /// <param name="filePath">The path to the assembly.</param>
233        /// <param name="assemblyName">The name of the assembly.</param>
234        private void LoadCorePlugin(string filePath, string assemblyName)
235        {
236            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(filePath);
237            if (assembly.GetName().FullName.Substring(0, assemblyName.Length + 1) !=
238                assemblyName + ",")
239            {
240                throw new FileLoadException(S._("The Core plugin assembly is not one which" +
241                    "Eraser expects.\n\nCheck that the Eraser installation is not corrupt, or " +
242                    "reinstall the program."));
243            }
244
245            //Create the PluginInstance structure
246            PluginInstance instance = new PluginInstance(assembly, null);
247
248            //Ignore non-plugins
249            if (!IsPlugin(instance.Assembly))
250                throw new FileLoadException(S._("The provided Core plugin assembly is not a " +
251                    "plugin.\n\nCheck that the Eraser installation is not corrupt, or reinstall " +
252                    "the program."));
253
254            //OK this assembly is a plugin
255            lock (plugins)
256                plugins.Add(instance);
257
258            //Check for the presence of a valid signature: Core plugins must have the same
259            //public key as the current assembly
260            if (!assembly.GetName().GetPublicKey().SequenceEqual(
261                    Assembly.GetExecutingAssembly().GetName().GetPublicKey()))
262            {
263                throw new FileLoadException(S._("The provided Core plugin does not have an " +
264                    "identical public key as the Eraser assembly.\n\nCheck that the Eraser " +
265                    "installation is not corrupt, or reinstall the program."));
266            }
267
268            //Okay, everything's fine, initialise the plugin
269            instance.Assembly = Assembly.Load(instance.Assembly.GetName());
270            instance.LoadingPolicy = LoadingPolicy.Core;
271            InitialisePlugin(instance);
272        }
273
274        public override bool LoadPlugin(string filePath)
275        {
276            //Create the PluginInstance structure
277            Assembly reflectAssembly = Assembly.ReflectionOnlyLoadFrom(filePath);
278            PluginInstance instance = new PluginInstance(reflectAssembly, null);
279
280            //Check that the plugin hasn't yet been loaded.
281            if (Plugins.Count(
282                    plugin => plugin.Assembly.GetName().FullName ==
283                    reflectAssembly.GetName().FullName) > 0)
284            {
285                return true;
286            }
287
288            //Ignore non-plugins
289            if (!IsPlugin(instance.Assembly))
290                return false;
291
292            //OK this assembly is a plugin
293            lock (plugins)
294                plugins.Add(instance);
295
296            //If the plugin does not have an approval or denial, check for the presence of
297            //a valid signature.
298            IDictionary<Guid, bool> approvals = ManagerLibrary.Settings.PluginApprovals;
299            if (!approvals.ContainsKey(instance.AssemblyInfo.Guid) &&
300                (reflectAssembly.GetName().GetPublicKey().Length == 0 ||
301                !Security.VerifyStrongName(filePath) ||
302                instance.AssemblyAuthenticode == null))
303            {
304                return false;
305            }
306
307            //Preliminary checks to verify whether the plugin can be loaded (safely) passes,
308            //Load the assembly fully, and then initialise it.
309            instance.Assembly = Assembly.Load(reflectAssembly.GetName());
310           
311            //The plugin either is explicitly allowed or disallowed to load, or
312            //it has an Authenticode Signature as well as a Strong Name. Get the
313            //loading policy of the plugin.
314            {
315               
316            }
317
318            bool initialisePlugin = false;
319
320            //Is there an approval or denial?
321            if (approvals.ContainsKey(instance.AssemblyInfo.Guid))
322                initialisePlugin = approvals[instance.AssemblyInfo.Guid];
323
324            //There's no approval or denial, what is the specified loading policy?
325            else
326                initialisePlugin = instance.LoadingPolicy != LoadingPolicy.DefaultOff;
327
328            if (initialisePlugin)
329            {
330                InitialisePlugin(instance);
331                return true;
332            }
333
334            return false;
335        }
336
337        /// <summary>
338        /// Initialises the given plugin from the plugin's description.
339        /// </summary>
340        /// <param name="instance">The <see cref="PluginInstance"/> structure to fill.</param>
341        private void InitialisePlugin(PluginInstance instance)
342        {
343            try
344            {
345                //Iterate over every exported type, checking for the IPlugin implementation
346                Type typePlugin = instance.Assembly.GetExportedTypes().First(
347                    type => type.GetInterface("Eraser.Manager.Plugin.IPlugin", true) != null);
348                if (typePlugin == null)
349                    return;
350
351                //Initialize the plugin
352                instance.Plugin = (IPlugin)Activator.CreateInstance(
353                    instance.Assembly.GetType(typePlugin.ToString()));
354                instance.Plugin.Initialize(this);
355
356                //And broadcast the plugin load event
357                OnPluginLoaded(this, new PluginLoadedEventArgs(instance));
358            }
359            catch (System.Security.SecurityException e)
360            {
361                MessageBox.Show(S._("Could not load the plugin {0}.\n\nThe error returned was: {1}",
362                    instance.Assembly.Location, e.Message), S._("Eraser"), MessageBoxButtons.OK,
363                    MessageBoxIcon.Error, MessageBoxDefaultButton.Button1,
364                    Localisation.IsRightToLeft(null) ?
365                        MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign : 0);
366            }
367        }
368
369        private Assembly AssemblyResolve(object sender, ResolveEventArgs args)
370        {
371            //Check the plugins folder
372            foreach (string fileName in Directory.GetFiles(PluginsFolder))
373            {
374                FileInfo file = new FileInfo(fileName);
375                if (file.Extension.Equals(".dll"))
376                    try
377                    {
378                        Assembly assembly = Assembly.ReflectionOnlyLoadFrom(file.FullName);
379                        if (assembly.GetName().FullName == args.Name)
380                            return Assembly.LoadFile(file.FullName);
381                    }
382                    catch (BadImageFormatException)
383                    {
384                    }
385                    catch (FileLoadException)
386                    {
387                    }
388            }
389
390            return null;
391        }
392
393        private Assembly ResolveReflectionDependency(object sender, ResolveEventArgs args)
394        {
395            return Assembly.ReflectionOnlyLoad(args.Name);
396        }
397
398        private List<PluginInstance> plugins = new List<PluginInstance>();
399    }
400
401    /// <summary>
402    /// Structure holding the instance values of the plugin like handle and path.
403    /// </summary>
404    public class PluginInstance
405    {
406        /// <summary>
407        /// Constructor
408        /// </summary>
409        /// <param name="assembly">The assembly representing this plugin.</param>
410        /// <param name="path">The path to the ass</param>
411        /// <param name="plugin"></param>
412        internal PluginInstance(Assembly assembly, IPlugin plugin)
413        {
414            Assembly = assembly;
415            Plugin = plugin;
416
417            //Verify the certificate in the assembly.
418            if (Security.VerifyAuthenticode(assembly.Location))
419            {
420                X509Certificate2 cert = new X509Certificate2(
421                    X509Certificate.CreateFromSignedFile(assembly.Location));
422                AssemblyAuthenticode = cert;
423            }
424        }
425
426        /// <summary>
427        /// Gets the Assembly this plugin instance came from.
428        /// </summary>
429        public Assembly Assembly
430        {
431            get
432            {
433                return assembly;
434            }
435            internal set
436            {
437                assembly = value;
438
439                AssemblyInfo info = new AssemblyInfo();
440                info.Version = assembly.GetName().Version;
441                IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(assembly);
442                foreach (CustomAttributeData attr in attributes)
443                    if (attr.Constructor.DeclaringType == typeof(GuidAttribute))
444                        info.Guid = new Guid((string)attr.ConstructorArguments[0].Value);
445                    else if (attr.Constructor.DeclaringType == typeof(AssemblyCompanyAttribute))
446                        info.Author = (string)attr.ConstructorArguments[0].Value;
447                    else if (attr.Constructor.DeclaringType == typeof(LoadingPolicyAttribute))
448                    {
449                        LoadingPolicy = (LoadingPolicy)attr.ConstructorArguments[0].Value;
450                        if (LoadingPolicy == LoadingPolicy.Core)
451                            LoadingPolicy = LoadingPolicy.None;
452                    }
453
454                this.AssemblyInfo = info;
455            }
456        }
457
458        /// <summary>
459        /// Gets the attributes of the assembly, loading from reflection-only sources.
460        /// </summary>
461        public AssemblyInfo AssemblyInfo { get; private set; }
462
463        /// <summary>
464        /// The Authenticode signature used for signing the assembly.
465        /// </summary>
466        public X509Certificate2 AssemblyAuthenticode { get; private set; }
467
468        /// <summary>
469        /// Gets whether the plugin is required for the functioning of Eraser (and
470        /// therefore cannot be disabled.)
471        /// </summary>
472        public LoadingPolicy LoadingPolicy { get; internal set; }
473
474        /// <summary>
475        /// Gets the IPlugin interface which the plugin exposed. This may be null
476        /// if the plugin was not loaded.
477        /// </summary>
478        public IPlugin Plugin { get; internal set; }
479
480        /// <summary>
481        /// Gets whether this particular plugin is currently loaded in memory.
482        /// </summary>
483        public bool Loaded
484        {
485            get { return Plugin != null; }
486        }
487
488        private Assembly assembly;
489    }
490
491    /// <summary>
492    /// Reflection-only information retrieved from the assembly.
493    /// </summary>
494    public struct AssemblyInfo
495    {
496        /// <summary>
497        /// The GUID of the assembly.
498        /// </summary>
499        public Guid Guid { get; set; }
500
501        /// <summary>
502        /// The publisher of the assembly.
503        /// </summary>
504        public string Author { get; set; }
505
506        /// <summary>
507        /// The version of the assembly.
508        /// </summary>
509        public Version Version { get; set; }
510
511        public override bool Equals(object obj)
512        {
513            if (!(obj is AssemblyInfo))
514                return false;
515            return Equals((AssemblyInfo)obj);
516        }
517
518        public bool Equals(AssemblyInfo other)
519        {
520            return Guid == other.Guid;
521        }
522
523        public static bool operator ==(AssemblyInfo assembly1, AssemblyInfo assembly2)
524        {
525            return assembly1.Equals(assembly2);
526        }
527
528        public static bool operator !=(AssemblyInfo assembly1, AssemblyInfo assembly2)
529        {
530            return !assembly1.Equals(assembly2);
531        }
532
533        public override int GetHashCode()
534        {
535            return Guid.GetHashCode();
536        }
537    }
538
539    /// <summary>
540    /// Basic plugin interface which allows for the main program to utilize the
541    /// functions in the DLL
542    /// </summary>
543    public interface IPlugin : IDisposable
544    {
545        /// <summary>
546        /// Initializer.
547        /// </summary>
548        /// <param name="host">The host object which can be used for two-way
549        /// communication with the program.</param>
550        void Initialize(Host host);
551
552        /// <summary>
553        /// The name of the plug-in, used for descriptive purposes in the UI
554        /// </summary>
555        string Name
556        {
557            get;
558        }
559
560        /// <summary>
561        /// The author of the plug-in, used for display in the UI and for users
562        /// to contact the author about bugs. Must be in the format:
563        ///     (.+) \&lt;([a-zA-Z0-9_.]+)@([a-zA-Z0-9_.]+)\.([a-zA-Z0-9]+)\&gt;
564        /// </summary>
565        /// <example>Joel Low <joel@joelsplace.sg></example>
566        string Author
567        {
568            get;
569        }
570
571        /// <summary>
572        /// Determines whether the plug-in is configurable.
573        /// </summary>
574        bool Configurable
575        {
576            get;
577        }
578
579        /// <summary>
580        /// Fulfil a request to display the settings for this plug-in.
581        /// </summary>
582        /// <param name="parent">The parent control which the settings dialog should
583        /// be parented with.</param>
584        void DisplaySettings(Control parent);
585    }
586
587    /// <summary>
588    /// Loading policies applicable for a given plugin.
589    /// </summary>
590    public enum LoadingPolicy
591    {
592        /// <summary>
593        /// The host decides the best policy for loading the plugin.
594        /// </summary>
595        None,
596
597        /// <summary>
598        /// The host will enable the plugin by default.
599        /// </summary>
600        DefaultOn,
601
602        /// <summary>
603        /// The host will disable the plugin by default
604        /// </summary>
605        DefaultOff,
606
607        /// <summary>
608        /// The host must always load the plugin.
609        /// </summary>
610        /// <remarks>This policy does not have an effect when declared in the
611        /// <see cref="LoadingPolicyAttribute"/> attribute and will be equivalent
612        /// to <see cref="None"/>.</remarks>
613        Core
614    }
615
616    /// <summary>
617    /// Declares the loading policy for the assembly containing the plugin. Only
618    /// plugins signed with an Authenticode signature will be trusted and have
619    /// this attribute checked at initialisation.
620    /// </summary>
621    [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
622    public sealed class LoadingPolicyAttribute : Attribute
623    {
624        /// <summary>
625        /// Constructor.
626        /// </summary>
627        /// <param name="policy">The policy used for loading the plugin.</param>
628        public LoadingPolicyAttribute(LoadingPolicy policy)
629        {
630            Policy = policy;
631        }
632
633        /// <summary>
634        /// The loading policy to be applied to the assembly.
635        /// </summary>
636        public LoadingPolicy Policy
637        {
638            get;
639            set;
640        }
641    }
642}
Note: See TracBrowser for help on using the repository browser.