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

Revision 2050, 18.1 KB checked in by lowjoel, 5 years ago (diff)

Implemented Core plugins as a list of plugins which must be loaded, and is dictated by the loading assembly. Addresses #363.

  • Plugins are always only loaded once
  • Only Core plugins, as defined by the loading assembly, is given the ability to be set as a Core plugin
  • 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            try
146            {
147                //Load all core plugins first
148                foreach (KeyValuePair<string, string> plugin in CorePlugins)
149                {
150                    LoadCorePlugin(Path.Combine(PluginsFolder, plugin.Key), plugin.Value);
151                }
152
153                //Then load the rest
154                foreach (string fileName in Directory.GetFiles(PluginsFolder))
155                {
156                    FileInfo file = new FileInfo(fileName);
157                    if (file.Extension.Equals(".dll"))
158                        try
159                        {
160                            LoadPlugin(file.FullName);
161                        }
162                        catch (BadImageFormatException)
163                        {
164                        }
165                        catch (FileLoadException)
166                        {
167                        }
168                }
169            }
170            finally
171            {
172                AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve;
173                AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= ResolveReflectionDependency;
174            }
175        }
176
177        protected override void Dispose(bool disposing)
178        {
179            if (plugins == null)
180                return;
181
182            if (disposing)
183            {
184                //Unload all the plugins. This will cause all the plugins to execute
185                //the cleanup code.
186                foreach (PluginInstance plugin in plugins)
187                    if (plugin.Plugin != null)
188                        plugin.Plugin.Dispose();
189            }
190
191            plugins = null;
192        }
193
194        /// <summary>
195        /// The path to the folder containing the plugins.
196        /// </summary>
197        public readonly string PluginsFolder = Path.Combine(
198            Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), //Assembly location
199            "Plugins" //Plugins folder
200        );
201
202        /// <summary>
203        /// The list of plugins which are core, the key is the file name, the value
204        /// is the assembly name.
205        /// </summary>
206        private readonly KeyValuePair<string, string>[] CorePlugins =
207            new KeyValuePair<string, string>[]
208            {
209                new KeyValuePair<string, string>(
210                    "Eraser.DefaultPlugins.dll",
211                    "Eraser.DefaultPlugins"
212                )
213            };
214
215        public override IList<PluginInstance> Plugins
216        {
217            get { return plugins.AsReadOnly(); }
218        }
219
220        /// <summary>
221        /// Verifies whether the provided assembly is a plugin.
222        /// </summary>
223        /// <param name="assembly">The assembly to verify.</param>
224        /// <returns>True if the assembly provided is a plugin, false otherwise.</returns>
225        private bool IsPlugin(Assembly assembly)
226        {
227            //Iterate over every exported type, checking if it implements IPlugin
228            Type typePlugin = assembly.GetExportedTypes().FirstOrDefault(
229                    type => type.GetInterface("Eraser.Manager.Plugin.IPlugin", true) != null);
230
231            //If the typePlugin type is empty the assembly doesn't implement IPlugin it's not
232            //a plugin.
233            return typePlugin != null;
234        }
235
236        /// <summary>
237        /// Loads the assembly at the specified path, and verifying its assembly name,
238        /// ensuring that the assembly contains a core plugin.
239        /// </summary>
240        /// <param name="filePath">The path to the assembly.</param>
241        /// <param name="assemblyName">The name of the assembly.</param>
242        private void LoadCorePlugin(string filePath, string assemblyName)
243        {
244            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(filePath);
245            if (assembly.GetName().FullName.Substring(0, assemblyName.Length + 1) !=
246                assemblyName + ",")
247            {
248                throw new FileLoadException(S._("The Core plugin assembly is not one which" +
249                    "Eraser expects.\n\nCheck that the Eraser installation is not corrupt, or " +
250                    "reinstall the program."));
251            }
252
253            //Create the PluginInstance structure
254            PluginInstance instance = new PluginInstance(assembly, null);
255
256            //Ignore non-plugins
257            if (!IsPlugin(instance.Assembly))
258                throw new FileLoadException(S._("The provided Core plugin assembly is not a " +
259                    "plugin.\n\nCheck that the Eraser installation is not corrupt, or reinstall " +
260                    "the program."));
261
262            //OK this assembly is a plugin
263            lock (plugins)
264                plugins.Add(instance);
265
266            //Check for the presence of a valid signature: Core plugins must have the same
267            //public key as the current assembly
268            if (!assembly.GetName().GetPublicKey().SequenceEqual(
269                    Assembly.GetExecutingAssembly().GetName().GetPublicKey()))
270            {
271                throw new FileLoadException(S._("The provided Core plugin does not have an " +
272                    "identical public key as the Eraser assembly.\n\nCheck that the Eraser " +
273                    "installation is not corrupt, or reinstall the program."));
274            }
275
276            //Okay, everything's fine, initialise the plugin
277            instance.Assembly = Assembly.Load(instance.Assembly.GetName());
278            instance.LoadingPolicy = LoadingPolicy.Core;
279            InitialisePlugin(instance);
280        }
281
282        public override bool LoadPlugin(string filePath)
283        {
284            //Create the PluginInstance structure
285            Assembly reflectAssembly = Assembly.ReflectionOnlyLoadFrom(filePath);
286            PluginInstance instance = new PluginInstance(reflectAssembly, null);
287
288            //Check that the plugin hasn't yet been loaded.
289            if (Plugins.Count(
290                    plugin => plugin.Assembly.GetName().FullName ==
291                    reflectAssembly.GetName().FullName) > 0)
292            {
293                return true;
294            }
295
296            //Ignore non-plugins
297            if (!IsPlugin(instance.Assembly))
298                return false;
299
300            //OK this assembly is a plugin
301            lock (plugins)
302                plugins.Add(instance);
303
304            //If the plugin does not have an approval or denial, check for the presence of
305            //a valid signature.
306            IDictionary<Guid, bool> approvals = ManagerLibrary.Settings.PluginApprovals;
307            if (!approvals.ContainsKey(instance.AssemblyInfo.Guid) &&
308                (reflectAssembly.GetName().GetPublicKey().Length == 0 ||
309                !Security.VerifyStrongName(filePath) ||
310                instance.AssemblyAuthenticode == null))
311            {
312                return false;
313            }
314
315            //Preliminary checks to verify whether the plugin can be loaded (safely) passes,
316            //Load the assembly fully, and then initialise it.
317            instance.Assembly = Assembly.Load(reflectAssembly.GetName());
318           
319            //The plugin either is explicitly allowed or disallowed to load, or
320            //it has an Authenticode Signature as well as a Strong Name. Get the
321            //loading policy of the plugin.
322            {
323               
324            }
325
326            bool initialisePlugin = false;
327
328            //Is there an approval or denial?
329            if (approvals.ContainsKey(instance.AssemblyInfo.Guid))
330                initialisePlugin = approvals[instance.AssemblyInfo.Guid];
331
332            //There's no approval or denial, what is the specified loading policy?
333            else
334                initialisePlugin = instance.LoadingPolicy != LoadingPolicy.DefaultOff;
335
336            if (initialisePlugin)
337            {
338                InitialisePlugin(instance);
339                return true;
340            }
341
342            return false;
343        }
344
345        /// <summary>
346        /// Initialises the given plugin from the plugin's description.
347        /// </summary>
348        /// <param name="instance">The <see cref="PluginInstance"/> structure to fill.</param>
349        private void InitialisePlugin(PluginInstance instance)
350        {
351            try
352            {
353                //Iterate over every exported type, checking for the IPlugin implementation
354                Type typePlugin = instance.Assembly.GetExportedTypes().First(
355                    type => type.GetInterface("Eraser.Manager.Plugin.IPlugin", true) != null);
356                if (typePlugin == null)
357                    return;
358
359                //Initialize the plugin
360                instance.Plugin = (IPlugin)Activator.CreateInstance(
361                    instance.Assembly.GetType(typePlugin.ToString()));
362                instance.Plugin.Initialize(this);
363
364                //And broadcast the plugin load event
365                OnPluginLoaded(this, new PluginLoadedEventArgs(instance));
366            }
367            catch (System.Security.SecurityException e)
368            {
369                MessageBox.Show(S._("Could not load the plugin {0}.\n\nThe error returned was: {1}",
370                    instance.Assembly.Location, e.Message), S._("Eraser"), MessageBoxButtons.OK,
371                    MessageBoxIcon.Error, MessageBoxDefaultButton.Button1,
372                    Localisation.IsRightToLeft(null) ?
373                        MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign : 0);
374            }
375        }
376
377        private Assembly AssemblyResolve(object sender, ResolveEventArgs args)
378        {
379            //Check the plugins folder
380            foreach (string fileName in Directory.GetFiles(PluginsFolder))
381            {
382                FileInfo file = new FileInfo(fileName);
383                if (file.Extension.Equals(".dll"))
384                    try
385                    {
386                        Assembly assembly = Assembly.ReflectionOnlyLoadFrom(file.FullName);
387                        if (assembly.GetName().FullName == args.Name)
388                            return Assembly.LoadFile(file.FullName);
389                    }
390                    catch (BadImageFormatException)
391                    {
392                    }
393                    catch (FileLoadException)
394                    {
395                    }
396            }
397
398            return null;
399        }
400
401        private Assembly ResolveReflectionDependency(object sender, ResolveEventArgs args)
402        {
403            return Assembly.ReflectionOnlyLoad(args.Name);
404        }
405
406        private List<PluginInstance> plugins = new List<PluginInstance>();
407    }
408
409    /// <summary>
410    /// Structure holding the instance values of the plugin like handle and path.
411    /// </summary>
412    public class PluginInstance
413    {
414        /// <summary>
415        /// Constructor
416        /// </summary>
417        /// <param name="assembly">The assembly representing this plugin.</param>
418        /// <param name="path">The path to the ass</param>
419        /// <param name="plugin"></param>
420        internal PluginInstance(Assembly assembly, IPlugin plugin)
421        {
422            Assembly = assembly;
423            Plugin = plugin;
424
425            //Verify the certificate in the assembly.
426            if (Security.VerifyAuthenticode(assembly.Location))
427            {
428                X509Certificate2 cert = new X509Certificate2(
429                    X509Certificate.CreateFromSignedFile(assembly.Location));
430                AssemblyAuthenticode = cert;
431            }
432        }
433
434        /// <summary>
435        /// Gets the Assembly this plugin instance came from.
436        /// </summary>
437        public Assembly Assembly
438        {
439            get
440            {
441                return assembly;
442            }
443            internal set
444            {
445                assembly = value;
446
447                AssemblyInfo info = new AssemblyInfo();
448                info.Version = assembly.GetName().Version;
449                IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(assembly);
450                foreach (CustomAttributeData attr in attributes)
451                    if (attr.Constructor.DeclaringType == typeof(GuidAttribute))
452                        info.Guid = new Guid((string)attr.ConstructorArguments[0].Value);
453                    else if (attr.Constructor.DeclaringType == typeof(AssemblyCompanyAttribute))
454                        info.Author = (string)attr.ConstructorArguments[0].Value;
455                    else if (attr.Constructor.DeclaringType == typeof(LoadingPolicyAttribute))
456                    {
457                        LoadingPolicy = (LoadingPolicy)attr.ConstructorArguments[0].Value;
458                        if (LoadingPolicy == LoadingPolicy.Core)
459                            LoadingPolicy = LoadingPolicy.None;
460                    }
461
462                this.AssemblyInfo = info;
463            }
464        }
465
466        /// <summary>
467        /// Gets the attributes of the assembly, loading from reflection-only sources.
468        /// </summary>
469        public AssemblyInfo AssemblyInfo { get; private set; }
470
471        /// <summary>
472        /// The Authenticode signature used for signing the assembly.
473        /// </summary>
474        public X509Certificate2 AssemblyAuthenticode { get; private set; }
475
476        /// <summary>
477        /// Gets whether the plugin is required for the functioning of Eraser (and
478        /// therefore cannot be disabled.)
479        /// </summary>
480        public LoadingPolicy LoadingPolicy { get; internal set; }
481
482        /// <summary>
483        /// Gets the IPlugin interface which the plugin exposed. This may be null
484        /// if the plugin was not loaded.
485        /// </summary>
486        public IPlugin Plugin { get; internal set; }
487
488        /// <summary>
489        /// Gets whether this particular plugin is currently loaded in memory.
490        /// </summary>
491        public bool Loaded
492        {
493            get { return Plugin != null; }
494        }
495
496        private Assembly assembly;
497    }
498
499    /// <summary>
500    /// Reflection-only information retrieved from the assembly.
501    /// </summary>
502    public struct AssemblyInfo
503    {
504        /// <summary>
505        /// The GUID of the assembly.
506        /// </summary>
507        public Guid Guid { get; set; }
508
509        /// <summary>
510        /// The publisher of the assembly.
511        /// </summary>
512        public string Author { get; set; }
513
514        /// <summary>
515        /// The version of the assembly.
516        /// </summary>
517        public Version Version { get; set; }
518
519        public override bool Equals(object obj)
520        {
521            if (!(obj is AssemblyInfo))
522                return false;
523            return Equals((AssemblyInfo)obj);
524        }
525
526        public bool Equals(AssemblyInfo other)
527        {
528            return Guid == other.Guid;
529        }
530
531        public static bool operator ==(AssemblyInfo assembly1, AssemblyInfo assembly2)
532        {
533            return assembly1.Equals(assembly2);
534        }
535
536        public static bool operator !=(AssemblyInfo assembly1, AssemblyInfo assembly2)
537        {
538            return !assembly1.Equals(assembly2);
539        }
540
541        public override int GetHashCode()
542        {
543            return Guid.GetHashCode();
544        }
545    }
546
547    /// <summary>
548    /// Basic plugin interface which allows for the main program to utilize the
549    /// functions in the DLL
550    /// </summary>
551    public interface IPlugin : IDisposable
552    {
553        /// <summary>
554        /// Initializer.
555        /// </summary>
556        /// <param name="host">The host object which can be used for two-way
557        /// communication with the program.</param>
558        void Initialize(Host host);
559
560        /// <summary>
561        /// The name of the plug-in, used for descriptive purposes in the UI
562        /// </summary>
563        string Name
564        {
565            get;
566        }
567
568        /// <summary>
569        /// The author of the plug-in, used for display in the UI and for users
570        /// to contact the author about bugs. Must be in the format:
571        ///     (.+) \&lt;([a-zA-Z0-9_.]+)@([a-zA-Z0-9_.]+)\.([a-zA-Z0-9]+)\&gt;
572        /// </summary>
573        /// <example>Joel Low <joel@joelsplace.sg></example>
574        string Author
575        {
576            get;
577        }
578
579        /// <summary>
580        /// Determines whether the plug-in is configurable.
581        /// </summary>
582        bool Configurable
583        {
584            get;
585        }
586
587        /// <summary>
588        /// Fulfil a request to display the settings for this plug-in.
589        /// </summary>
590        /// <param name="parent">The parent control which the settings dialog should
591        /// be parented with.</param>
592        void DisplaySettings(Control parent);
593    }
594
595    /// <summary>
596    /// Loading policies applicable for a given plugin.
597    /// </summary>
598    public enum LoadingPolicy
599    {
600        /// <summary>
601        /// The host decides the best policy for loading the plugin.
602        /// </summary>
603        None,
604
605        /// <summary>
606        /// The host will enable the plugin by default.
607        /// </summary>
608        DefaultOn,
609
610        /// <summary>
611        /// The host will disable the plugin by default
612        /// </summary>
613        DefaultOff,
614
615        /// <summary>
616        /// The host must always load the plugin.
617        /// </summary>
618        /// <remarks>This policy does not have an effect when declared in the
619        /// <see cref="LoadingPolicyAttribute"/> attribute and will be equivalent
620        /// to <see cref="None"/>.</remarks>
621        Core
622    }
623
624    /// <summary>
625    /// Declares the loading policy for the assembly containing the plugin. Only
626    /// plugins signed with an Authenticode signature will be trusted and have
627    /// this attribute checked at initialisation.
628    /// </summary>
629    [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
630    public sealed class LoadingPolicyAttribute : Attribute
631    {
632        /// <summary>
633        /// Constructor.
634        /// </summary>
635        /// <param name="policy">The policy used for loading the plugin.</param>
636        public LoadingPolicyAttribute(LoadingPolicy policy)
637        {
638            Policy = policy;
639        }
640
641        /// <summary>
642        /// The loading policy to be applied to the assembly.
643        /// </summary>
644        public LoadingPolicy Policy
645        {
646            get;
647            set;
648        }
649    }
650}
Note: See TracBrowser for help on using the repository browser.