Chapter 38. Designing an Extensible Plugin-Based Architecture

 

When I am working on a problem, I never think about beauty. I think only of how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong.

 
 --R. Buckminster Fuller

Many applications provide a mechanism to support extensibility through the use of external code modules, also known as plugins, which are linked into the application at runtime. Plugin support is generally used so that the application can be extended with additional functionality without the need to recompile the source code and distribute the executable to users. Some applications have business rules that change frequently, or they have new business rules added on a regular basis. Plugins allow business rules to be added or changed easily without recompilation or redistribution.

The .NET framework and the Common Language Runtime provide a variety of classes and mechanisms for dynamically loading assemblies at runtime and peering into the metadata of these assemblies. This dynamic support makes .NET an ideal platform for plugin-based architectures.

Making your application plugin-aware is also an excellent way to promote a longer lifetime. While not all applications are suitable for this kind of architecture, many are, especially within tools development. This chapter will cover the rudiments and advanced topics surrounding plugin-based architectures and the .NET platform.

Designing a Common Interface

Each plugin is unique in terms of functionality, but there must be some common elements between all plugins in order to load them with a generic framework, and this is best accomplished through the use of interfaces. Interfaces are reference types and contain only abstract members. Interfaces are, in essence, a contract, so any classes implementing an interface are enforced to implement all members on the interface. This means that an application only needs to know about the interface in order to communicate with the class.

Information about classes can also be inspected with reflection, so we can use this common interface’s type to dynamically locate plugin classes within an assembly. Classes that do not implement this common interface will not be loaded as plugins.

The common interface should be placed in a separate assembly so that the application and the plugins can reference the interface as a shared assembly. You can even go a step further by placing the shared assembly in the Global Assembly Cache so that the application and plugins do not need local copies to compile or run.

The following code shows the common interface from the example for this chapter. The two methods are used, respectively, to initialize and release plugin resources. The actual implementation details for these two methods are left up to the individual plugins.

namespace Plugin.API
{
    public interface IPlugin
    {
        void Initialize();
        void Release();
    }
}

Embedding Plugin Metadata Information

A common feature of most plugin-aware applications is a plugin manager or browser that can display a listing of all the plugins referenced by the application. Some of these browsers go even further by showing information about each plug-in to describe functionality and author credits.

The same functionality can easily be accomplished by decorating a plugin with an attribute that describes what the plugin does.

Figure 38.1 shows an example of a plugin browser. The Component property describes the short name of the plugin, while the Description property provides a more lengthy description of what the plugin actually does.

Screenshot of the plugin browser example.

Figure 38.1. Screenshot of the plugin browser example.

The following code is for the Calculator Subtraction Plugin shown in Figure 38.1. Notice the PluginAttribute decoration.

namespace Plugin.CalculatorPlugin
{
    using API;
    using ExampleInterfaces;
    [Plugin(Component = "Calculator Subtraction Plugin",
    Description = "This plugin handles the subtraction operator")]
    public class CalculatorSubtraction : MarshalByRefObject,
                                         IPlugin,
                                         ICalculatorPlugin
    {
        public void Initialize()
        {
        }
        public void Release()
        {
        }
        public double Operation(double left, double right)
        {
            return left - right;
        }
    }
}

Note

You may have noticed that the class is decorated with Plugin instead of PluginAttribute. It is optional to append the Attribute text when decorating a class with an attribute.

The following code shows the PluginAttribute decoration. It would be very easy to add to this attribute over time if there is additional information you want to be embedded with each plugin.

namespace Plugin.API
{
    [AttributeUsage(AttributeTargets.Class)]
    public class PluginAttribute : Attribute
    {
        private string component = string.Empty;
        private string description = string.Empty;
        public string Component
        {
            get { return component; }
            set { component = value; }
        }
        public string Description
        {
            get { return description; }
            set { description = value; }
        }
    }
}

The PluginAttribute decoration should be located in the same assembly as the IPlugin interface.

Building a Proxy Wrapper

Here is where things get slightly more complicated. Typically, many of the plugin systems created with .NET load external plugin assemblies into the main AppDomain of the application. Although this works and you can use event-driven plugins to your heart’s content, these external assemblies can never be unloaded from the main AppDomain until the application quits. With a lot of loaded plugins, especially plugins that are only run for a short period of time, the AppDomain can quickly become swamped with loaded assemblies that degrade performance and consume valuable system memory.

The solution to this problem does require additional work, but the external plug-in assemblies will be unloadable. By creating another AppDomain, you can load external assemblies into it, execute the needed functionality, and then dump the extra AppDomain when the plugin is no longer needed. There is a catch though. There is no restriction that can be enforced to stop types within the extra AppDomain from leaking into the main AppDomain in certain situations.

One particular situation, which frustrated me and required a code refactoring, was to avoid the use of delegates and events between the two AppDomains. Doing so will load the plugin class into the main AppDomain, preventing the extra AppDomain from being unloaded.

You cannot directly instantiate a plugin class using the Activator object directly from the main AppDomain. You must use a MarshalByRefObject that will serve as a proxy between the two AppDomains. It is also important that you do not return any object references or types from the proxy class. Use only types that are available to the main AppDomain (strings to represent type and interface names, for example).

The extra AppDomain is not found in the proxy class, because the proxy class will be instantiated within the extra AppDomain. The following code describes the proxy wrapper; the extra AppDomain code will be covered in the next section.

using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
namespace Plugin.Manager
{
    using Plugin.API;
    public class PluginProxy : MarshalByRefObject
    {
        List<Type> pluginTypes = new List<Type>();
        List<PluginInfo> pluginInfo = new List<PluginInfo>();
        List<IPlugin> pluginInstances = new List<IPlugin>();

The following method is used to load a plugin assembly into a temporary AppDomain and build a collection of information about each class that implements the IPlugin interface.

        public bool LoadAssembly(AppDomain appDomain, byte[] data)
        {
            try
            {
                 Assembly assembly = appDomain.Load(data);
                 foreach (Type type in assembly.GetTypes())
                 {
                     if (!type.IsAbstract)
                     {
                         foreach (Type interfaceType in type.GetInterfaces())
                         {
                             if (interfaceType == typeof(IPlugin) &&
                                 type.IsDefined(typeof(PluginAttribute), false))
                             {
                                 pluginTypes.Add(type);
                                 PluginAttribute pluginAttrib =
                               type.GetCustomAttributes(typeof(PluginAttribute),
                                                   false)[0] as PluginAttribute;

As mentioned earlier, it is important that the proxy does not return any of the types within the plugin library. We want users to be able to view the plugin attribute information about each plugin, so the following two lines instantiate a wrapper class designed to hold the attribute information.

                         PluginInfo info = new PluginInfo(pluginAttrib.Component,
                                                    pluginAttrib.Description);
                                 pluginInfo.Add(info);
                             }
                         }
                     }
                 }
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

The following method is used to determine whether the plugin assembly contains any plugin classes that implement a particular interface. This is used to determine which assemblies can handle a particular application component.

        public bool ImplementsInterface(string interfaceName)
        {
            foreach (Type type in pluginTypes)
            {
                 foreach (Type interfaceType in type.GetInterfaces())
                 {
                     if (interfaceType.Name.Equals(interfaceName))
                         return true;
                 }
            }
            return false;
        }

The following method is used to instantiate all the plugins within an assembly, execute the Initialize() method, and add the instantiated plugins to a list to keep track of them.

        public void Initialize()
        {
             bool exists = false;
             foreach (Type type in pluginTypes)
             {
                foreach (IPlugin plugin in pluginInstances)
                {
                    if (plugin.GetType().Equals(type))
                    {
                        exists = true;
                        break;
                    }
                }
                if (!exists)
                {
                     IPlugin plugin = Activator.CreateInstance(type) as IPlugin;
                     ExecuteInitializeMethod(plugin);
                     pluginInstances.Add(plugin);
                }
                exists = false;
             }
        }

The following method loops through all the instantiated plugins and calls the Release() method.

        public void Release()
        {
            foreach (IPlugin plugin in pluginInstances)
            {
                ExecuteReleaseMethod(plugin);
            }
        }

The following method is a wrapper around executing a method of the plugin with no return value. Remember that we cannot access objects directly from outside of the proxy.

        public void ExecuteMethodNoReturn(string interfaceName,
                                          string method,
                                          object[] parameters)
       {
            foreach (IPlugin plugin in pluginInstances)
            {
                foreach (Type interfaceType in plugin.GetType().GetInterfaces())
                {
                    if (interfaceType.Name.Equals(interfaceName))
                    {
                        ExecuteMethodNoReturn(plugin,
                                              method,
                                              parameters);
                    }
                }
            }
       }

The following method is a wrapper around executing a method of the plugin, except this time with return values.

       public object[] ExecuteMethodWithReturn(string interfaceName,
                                               string method,
                                               object[] parameters)
       {
           List<object> results = new List<object>();
           foreach (IPlugin plugin in pluginInstances)
           {
               foreach (Type interfaceType in plugin.GetType().GetInterfaces())
               {
                   if (interfaceType.Name.Equals(interfaceName))
                   {
                       results.Add(ExecuteMethodWithReturn(plugin,
                                                           method,
                                                           parameters));
                   }
               }
           }
           return results.ToArray();
       }

The following method is used to return metadata information about all the plugins within the assembly.

       public PluginInfo[] QueryPluginInformation()
       {
           return pluginInfo.ToArray();
       }
       #region Plugin Method Invocation

The following method uses reflection to call the Initialize() method directly on the IPlugin instance.

       private void ExecuteInitializeMethod(IPlugin plugin)
       {
           ExecuteMethodNoReturn(plugin, "Initialize", null);
       }

The following method uses reflection to call the Release() method directly on the IPlugin instance.

        private void ExecuteReleaseMethod(IPlugin plugin)
        {
            ExecuteMethodNoReturn(plugin, "Release", null);
        }

The following method uses reflection to call a method directly on the IPlugin instance. This call does not return any values.

        private void ExecuteMethodNoReturn(IPlugin plugin,
                                           string methodName,
                                           object[] parameters)
        {
            MethodInfo method = plugin.GetType().GetMethod(methodName);
            if (method != null)
                method.Invoke(plugin, parameters);
        }

The following method uses reflection to call a method directly on the IPlugin instance. This call returns values.

        private object ExecuteMethodWithReturn(IPlugin plugin,
                                               string methodName,
                                               object[] parameters)
       {
            MethodInfo method = plugin.GetType().GetMethod(methodName);
            if (method != null)
                return method.Invoke(plugin, parameters);
            return null;
       }
       #endregion
    }
}

Loading Plugins Through the Proxy

With the proxy created, we can now move on to loading plugins and accessing them through the proxy. The following class wraps each plugin assembly and routes messages to and from the proxy object. This class also handles a temporary AppDomain used to load the plugin assembly independent from the main AppDomain.

using System;
using System.IO;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;
using System.Collections;
namespace Plugin.Manager
{
     using Plugin.API;
     public sealed class PluginLibrary
     {
         private AppDomain appDomain;
         private PluginProxy proxy;
         private string name = string.Empty;
         public string Name
         {
             get { return name; }
         }

The following method is used to load a plugin assembly from the file system into memory, create a temporary AppDomain with enforced security permissions, and instantiate the proxy object within the temporary AppDomain. If the plugin file is source code, then the plugin is compiled and loaded afterwards. The supported source code languages are C#, VB.NET, and J#. The default settings for the security policy will deny the ability to compile source code at runtime, but you can either change the policy file or disable security by commenting out EnforceSecurityPolicy(). Remember to only do this in a fully trusted environment.

         public bool Load(DirectoryInfo pluginDirectory, FileInfo plugin)
         {
             try
             {
                  if (plugin.Exists)
                  {
                      using (FileStream stream = plugin.OpenRead())
                  {
                      byte[] assemblyData = new byte[stream.Length];
                      if (stream.Read(assemblyData,
                                                0,
                                               (int)stream.Length) < 1)
                      {
                          return false;
                      }
                      AppDomainSetup setup = new AppDomainSetup();
                      setup.ApplicationName = "Plugins";
                      setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
                      setup.ShadowCopyFiles = "true";
                      setup.ShadowCopyDirectories = pluginDirectory.FullName;
                      appDomain = AppDomain.CreateDomain("PluginDomain" +
                      plugin.Name.Replace(".dll", "").Replace(".", ""), null, setup);
                      EnforceSecurityPolicy();
                      proxy = appDomain.CreateInstanceAndUnwrap("Plugin.Manager",
                                      "Plugin.Manager.PluginProxy") as PluginProxy;
                      if (plugin.Extension.EndsWith("cs") ||
                          plugin.Extension.EndsWith("js") ||
                          plugin.Extension.EndsWith("vb"))
                      {
                          if (!proxy.CompileAssembly(appDomain, plugin.FullName))
                          {
                              return false;
                          }
                      }
                      else if (!proxy.LoadAssembly(appDomain, assemblyData))
                      {
                          return false;
                      }
                      name = plugin.Name;
                      return true;
                  }
                  }
                  else
                  {
                      return false;
                  }
             }
             catch (IOException)
             {
                 return false;
             }
         }
         public void Unload()
         {
             if (appDomain == null)
                 return;
             Release();
             AppDomain.Unload(appDomain);
             appDomain = null;
         }
         public PluginInfo[] QueryPluginInformation()
         {
             return proxy.QueryPluginInformation();
         }
         public void Initialize()
         {
             proxy.Initialize();
         }
         public void Release()
         {
             proxy.Release();
         }
         public bool ImplementsInterface(string interfaceName)
         {
             return proxy.ImplementsInterface(interfaceName);
         }
         public bool ImplementsInterface(Type interfaceType)
         {
             return proxy.ImplementsInterface(interfaceType.Name);
         }
         public void ExecuteMethodNoReturn(string interfaceName,
                                           string methodName,
                                           object[] parameters)
         {
             proxy.ExecuteMethodNoReturn(interfaceName,
                                         methodName,
                                         parameters);
         }
         public void ExecuteMethodNoReturn(Type interfaceType,
                                           string methodName,
                                           object[] parameters)
         {
             proxy.ExecuteMethodNoReturn(interfaceType.Name,
                                         methodName,
                                         parameters);
         }
         public object[] ExecuteMethodWithReturn(string interfaceName,
                                                 string methodName,
                                                 object[] parameters)
         {
             return proxy.ExecuteMethodWithReturn(interfaceName,
                                                  methodName,
                                                  parameters);
         }
         public object[] ExecuteMethodWithReturn(Type interfaceType,
                                                 string methodName,
                                                 object[] parameters)
         {
             return proxy.ExecuteMethodWithReturn(interfaceType.Name,
                                                  methodName,
                                                  parameters);
         }

The following two methods are covered later on in the “Enforcing a Security Policy” section.

         private void EnforceSecurityPolicy()
         {
             IMembershipCondition condition;
             PolicyStatement statement;
             PolicyLevel policyLevel = PolicyLevel.CreateAppDomainLevel();
             PermissionSet permissionSet = new PermissionSet(PermissionState.None);
             SecurityPermission permission
             = new SecurityPermission(SecurityPermissionFlag.Execution)
             permissionSet.AddPermission(permission);
             condition = new AllMembershipCondition();
             statement = new PolicyStatement(permissionSet,
                                             PolicyStatementAttribute.Nothing);
             // The root code group of the policy level combines all
             // permissions of its children.
             UnionCodeGroup codeGroup = new UnionCodeGroup(condition, statement);
             NamedPermissionSet localIntranet
                                        = FindNamedPermissionSet("LocalIntranet");
             condition = new ZoneMembershipCondition(SecurityZone.MyComputer);
             statement = new PolicyStatement(localIntranet,
                                                PolicyStatementAttribute.Nothing);
             // The following code limits all code on this machine
             // to local intranet permissions when running in this
             // application domain.
             UnionCodeGroup virtualIntranet = new UnionCodeGroup(condition,
                                                                     statement);
             virtualIntranet.Name = "Virtual Intranet";
             // Add the code groups to the policy level.
             codeGroup.AddChild(virtualIntranet);
             policyLevel.RootCodeGroup = codeGroup;
             appDomain.SetAppDomainPolicy(policyLevel);
         }
         private NamedPermissionSet FindNamedPermissionSet(string name)
         {
             IEnumerator policyEnumerator = SecurityManager.PolicyHierarchy();
             while (policyEnumerator.MoveNext())
             {
                 PolicyLevel currentLevel = policyEnumerator.Current
                                                                  as PolicyLevel;
                 if (currentLevel.Label == "Machine")
                 {
                     IList namedPermissions = currentLevel.NamedPermissionSets;
                     IEnumerator namedPerm = namedPermissions.GetEnumerator();
                     while (namedPerm.MoveNext())
                     {
                        if (((NamedPermissionSet)namedPerm.Current).Name == name)
                         {
                             return ((NamedPermissionSet)namedPerm.Current);
                         }
                     }
                 }
             }
             return null;
         }
    }
}

Each instance of the plugin library class represents a plugin assembly or source file in the plugins directory. Therefore, each plugin has its own temporary AppDomain that can be unloaded at will without affecting the rest of the system.

Reloading Plugins During Runtime

The majority of plugin-enabled applications load and initialize all plugins when the application first launches, but plugins would not be reloaded if they had changed on the file system. The new version of the plugins would not be visible until the application had relaunched. It would be even better if the application could detect file system changes and automatically reload plugins that had changed. This would greatly speed up plugin debugging and development.

The following class keeps track of the loaded plugins, but it also contains the code to watch the file system for changes, reloading the plugins when appropriate.

using System;
using System.IO;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;
namespace Plugin.Manager
{
    public class PluginCatalogue
    {
        private FileSystemWatcher fileSystemWatcher = null;
        private string lockObject = "{RELOAD_PLUGINS_LOCK}";
        private DateTime changeTime = new DateTime(0);
        private Thread pluginReloadThread = null;
        private readonly List<PluginLibrary> plugins
                                                = new List<PluginLibrary>();
        private bool beginShutdown = false;
        private bool active = true;
        private bool started = false;
        private bool autoReload = true;
        private string pluginDirectory = string.Empty;
        public event EventHandler ReloadedPlugins;
        public event EventHandler UnloadedPlugins;
        public List<PluginLibrary> Plugins
        {
            get { return plugins; }
        }

The following property is used to stop and start automatic plugin reloading at runtime, which is useful if you want to make it a user setting.

        public bool AutoReload
        {
             get
             {
                  return autoReload;
             }
             set
            {
                 if (autoReload != value)
                 {
                     autoReload = value;
                     if (!autoReload)
                     {
                         fileSystemWatcher.EnableRaisingEvents = false;
                         ReleasePluginRuntime();
                         pluginReloadThread = null;
                         fileSystemWatcher = null;
                     }
                     else
                     {
                         CreateFileSystemWatcherAndThread();
                     }
                 }
            }
        }
        public PluginCatalogue(string pluginDirectory)
        {
            this.pluginDirectory = pluginDirectory;
        }
        public void FireUnloadEvent()
        {
            if (UnloadedPlugins != null)
                UnloadedPlugins(this, EventArgs.Empty);
        }

The following method creates the FileSystemWatcher object, points it at the plugin directory, and binds the event handlers to the appropriate method. The reload plugin thread is also created here.

        private void CreateFileSystemWatcherAndThread()
        {
            DirectoryInfo directory = new DirectoryInfo(pluginDirectory);
            if (!directory.Exists)
                directory.Create();
            fileSystemWatcher = new FileSystemWatcher(pluginDirectory);
            fileSystemWatcher.EnableRaisingEvents = true;
            fileSystemWatcher.Changed
               += new FileSystemEventHandler(fileSystemWatcher_Changed);
            fileSystemWatcher.Deleted
                += new FileSystemEventHandler(fileSystemWatcher_Changed);
            fileSystemWatcher.Created
                += new FileSystemEventHandler(fileSystemWatcher_Changed);
            pluginReloadThread
                = new Thread(new ThreadStart(this.ReloadPluginsThread));
            pluginReloadThread.Start();
        }

The following method is used to get a listing of valid plugin files from the plugin directory. Then the plugins themselves are loaded, initialized, and added to the plugin list.

        private void LoadPluginDirectory()
        {
            UnloadPluginDirectory();
            DirectoryInfo pluginDirectoryInfo
                                          = new DirectoryInfo(pluginDirectory);
            foreach (FileInfo pluginFile
                     in GetPluginFiles(pluginDirectoryInfo))
            {
                PluginLibrary plugin = LoadPlugin(pluginDirectoryInfo,
                                                  pluginFile);
                if (plugin != null)
                {
                    plugin.Initialize();
                    plugins.Add(plugin);
                }
            }
        }

The following method is used to unload all the plugins and clear the plugin list. This method is generally used when reloading plugins.

        private void UnloadPluginDirectory()
        {
            bool subsequentCall = false;
            foreach (PluginLibrary existingLibrary in plugins)
            {
                subsequentCall = true;
                existingLibrary.Unload();
            }
            plugins.Clear();
            if (subsequentCall)
            {
                if (UnloadedPlugins != null)
                    UnloadedPlugins(this, EventArgs.Empty);
            }
        }

The following method is the starting point for the system at runtime. This method will create the FileSystemWatcher object and the reload plugins thread, after which the plugins are loaded from the plugins directory.

        public void InitializePluginRuntime()
        {
            started = true;
            if (autoReload)
            {
                CreateFileSystemWatcherAndThread();
            }
            ReloadPlugins();
        }

The following method is used to stop the FileSystemWatcher and the reload plugins thread, unloading all the plugins in the process.

        public void ReleasePluginRuntime()
        {
            try
            {
                started = false;
                UnloadPluginDirectory();
                beginShutdown = true;
                while (active)
                {
                     Thread.Sleep(100);
                }
            }
            catch
            {
                //Quietly ignore unload exceptions
            }
        }

The following method is the logic for the reload plugins thread. This method continuously loops while active, and the plugins are reloaded when the change time is set by the FileSystemWatcher object.

        protected void ReloadPluginsThread()
        {
            if (!started)
            {
              throw new InvalidOperationException("PluginManager not started.");
            }
            DateTime invalidTime = new DateTime(0);
            while (!beginShutdown)
            {
                if (changeTime != invalidTime && DateTime.Now > changeTime)
                {
                    ReloadPlugins();
                }
                Thread.Sleep(5000);
            }
            active = false;
        }

The following method is invoked from the ReloadPluginsThread method, and is used to reload the plugin list from the plugins directory.

        private void ReloadPlugins()
        {
            if (!started)
            {
               throw new InvalidOperationException("PluginManager not started.");
            }
            lock (lockObject)
            {
                LoadPluginDirectory();
                changeTime = new DateTime(0);
                if (ReloadedPlugins != null)
                    ReloadedPlugins(this, EventArgs.Empty);
            }
        }

The following method is the event handler for the FileSystemWatcher object. This handler is invoked whenever the plugin directory changes. A new change time is set so that the reload thread will fire 10 seconds from the current time.

        void fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            changeTime = DateTime.Now + new TimeSpan(0, 0, 10);
        }

The following method is used to load a plugin from the specified path. A new PluginLibrary instance is created for the plugin and returned if successful.

        private PluginLibrary LoadPlugin(DirectoryInfo pluginDirectory,
                                         FileInfo pluginFile)
        {
            bool success = false;
            PluginLibrary plugin = null;
            try
            {
                plugin = new PluginLibrary();
                if (plugin.Load(pluginDirectory, pluginFile))
                {
                    success = true;
                }
            }
            catch (Exception)
            {
                success = false;
            }
            if (!success)
            {
                MessageBox.Show(String.Format("Could not load plugin [{0}].",
                                               pluginFile.Name));
                plugin = null;
            }
            return plugin;
        }

The following method is used to check the loaded plugin list and see if any of the instances support the specified interface. This is used to return a list of compatible plugins for the given interface. This method can be considered a caching optimization so that the entire plugin list does not have to be checked when invoking a method on a plugin interface.

        public List<PluginLibrary> DeterminePluginTargets(Type interfaceTarget)
        {
            List<PluginLibrary> targets = new List<PluginLibrary>();
            foreach (PluginLibrary library in plugins)
            {
                if (library.ImplementsInterface(interfaceTarget.Name))
                {
                    targets.Add(library);
                }
            }
            return targets;
        }

The following method is used to return a list of valid plugin files from the specified directory. This list will be the one that the plugin catalogue uses to load all the plugins.

        private FileInfo[] GetPluginFiles(DirectoryInfo pluginDirectory)
        {
            FileInfo[] plugins;
            if (pluginDirectory.Exists)
            {
                List<FileInfo> filteredPlugins = new List<FileInfo>();
                filteredPlugins.AddRange(pluginDirectory.GetFiles("*.dll"));
                filteredPlugins.AddRange(pluginDirectory.GetFiles("*.cs"));
                filteredPlugins.AddRange(pluginDirectory.GetFiles("*.vb"));
                filteredPlugins.AddRange(pluginDirectory.GetFiles("*.js"));
                plugins = filteredPlugins.ToArray();
            }
            else
            {
                pluginDirectory.Create();
                plugins = new FileInfo[0];
            }
            return plugins;
        }
    }
 }

Runtime Compilation of Plugins

Plugins are an excellent way to extend an application without the need to recompile the application. However, the plugins themselves must be recompiled when modified, and the new version must then be deployed to the installation applications. We can try to improve this by introducing a runtime plugin compiler into the solution. Doing so will allow us to place source code files in the plugins directory and have them loaded into the application at runtime as compiled assemblies. Obviously, you would only want to use this feature in a trusted environment. In fact, the security policy introduced in this chapter will not permit this kind of code to execute without readjusting the default settings.

The following code shows the full source code to the plugin factory that is used to compile plugins at runtime. The only real functionality is in CompilePluginSource(), where the appropriate CodeDom compiler is created, and many of the most commonly used class framework libraries are referenced so that they are available to the plugins.

using System;
using System.IO;
using System.Reflection;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
namespace Plugin.Manager
{
     internal class PluginFactory
     {
         private CompilerErrorCollection compileErrors
               = new CompilerErrorCollection();
         public CompilerErrorCollection CompileErrors
         {
             get { return compileErrors; }
         }
         public Assembly CompilePluginSource(string fileName)
         {
             return CompilePluginSource(new List<string>
                                        (new string[]
                                        { fileName }),
                                        null);
         }
         public Assembly CompilePluginSource(List<string> fileNames)
         {
             return CompilePluginSource(fileNames, null);
         }
         public Assembly CompilePluginSource(string fileName,
                                             List<string> references)
         {
             return CompilePluginSource(new List<string>
                                        (new string[]
                                        { fileName }),
                                        references);
         }
         public Assembly CompilePluginSource(List<string> fileNames,
                                             List<string> references)

         {
              string fileType = null;
              foreach (string fileName in fileNames)
              {
                  string extension = Path.GetExtension(fileName);
                  if (fileType == null)
                  {
                      fileType = extension;
                  }
                  else if (fileType != extension)
                  {
                      throw new ArgumentException("All source code files must be " +
                                                 "written in the same language!");
                  }
              }
              CodeDomProvider codeProvider = null;
              switch (fileType)
              {
                  case ".cs":
                  {
                      codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
                      break;
                  }
                  case ".vb":
                  {
                      codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
                      break;
                  }
                  case ".js":
                  {
                      codeProvider = new Microsoft.VJSharp.VJSharpCodeProvider();
                       break;
                  }
                  default:
                  {
                       throw new InvalidOperationException("Invalid source code " +
                                                               "file extension!");
                  }
               }
               CompilerParameters parameters = new CompilerParameters();
               parameters.CompilerOptions = "/target:library /optimize";
               parameters.GenerateExecutable = false;
               parameters.GenerateInMemory = true;
               parameters.IncludeDebugInformation = false;
               parameters.ReferencedAssemblies.Add("mscorlib.dll");
               parameters.ReferencedAssemblies.Add("System.dll");
               parameters.ReferencedAssemblies.Add("Plugin.API.dll");
               parameters.ReferencedAssemblies.Add("Plugin.ExampleInterfaces.dll");
               parameters.ReferencedAssemblies.Add("System.Configuration.Install.dll");
               parameters.ReferencedAssemblies.Add("System.Data.dll");
               parameters.ReferencedAssemblies.Add("System.Design.dll");
               parameters.ReferencedAssemblies.Add("System.DirectoryServices.dll");
               parameters.ReferencedAssemblies.Add("System.Drawing.Design.dll");
               parameters.ReferencedAssemblies.Add("System.Drawing.dll");
               parameters.ReferencedAssemblies.Add("System.EnterpriseServices.dll");
               parameters.ReferencedAssemblies.Add("System.Management.dll");
               parameters.ReferencedAssemblies.Add("System.Runtime.Remoting.dll");
               parameters.ReferencedAssemblies.Add(
                    "System.Runtime.Serialization.Formatters.Soap.dll");
               parameters.ReferencedAssemblies.Add("System.Security.dll");
               parameters.ReferencedAssemblies.Add("System.ServiceProcess.dll");
               parameters.ReferencedAssemblies.Add("System.Web.dll");
               parameters.ReferencedAssemblies.Add("System.Web.RegularExpressions.dll");
               parameters.ReferencedAssemblies.Add("System.Web.Services.dll");
               parameters.ReferencedAssemblies.Add("System.Windows.Forms.Dll");
               parameters.ReferencedAssemblies.Add("System.XML.dll");
               parameters.ReferencedAssemblies.Add("Accessibility.dll");
               parameters.ReferencedAssemblies.Add("Microsoft.Vsa.dll");
               if (references != null)
               {
                   foreach (string reference in references)
                   {
                       if (!parameters.ReferencedAssemblies.Contains(reference))
                       {
                           parameters.ReferencedAssemblies.Add(reference);
                       }
                   }
               }
               CompilerResults results
                      = codeProvider.CompileAssemblyFromFile(parameters,
                                                             fileNames.ToArray());
               compileErrors = results.Errors;
               if (compileErrors.Count > 0)
               {
                   throw new Exception("Error(s) occurred while " +
                                                      "compiling source file(s).");
               }
               return results.CompiledAssembly;
         }
     }
}

Note

One restriction on the code compilation system presented in this chapter is that each source code file must be a fully functional module. Plugins cannot be spread over multiple files in the plugins directory, because each file will be compiled as a standalone plugin.

Enforcing a Security Policy

A common concern when making an application plugin-aware is how code security can be handled. Malicious plugins can do a lot of damage to the application or even the operating system itself. This is even more of a concern when allowing dynamic runtime compilation of plugins. There are two main approaches to solving this problem with the .NET platform: code access security and setting a security policy on the temporary AppDomain that plugins run under. This chapter will not cover code access security, but it will discuss how to enforce a security policy.

Windows has a variety of security zones that restrict what applications and web sites can do under them. The actual restrictions for these security zones can be customized to your needs.

The following two methods show how to set the security policy of the temporary AppDomain so that code within it runs under the Local Intranet security zone.

private void EnforceSecurityPolicy()
{
    IMembershipCondition condition;
    PolicyStatement statement;
    PolicyLevel policyLevel = PolicyLevel.CreateAppDomainLevel();
    PermissionSet permissionSet = new PermissionSet(PermissionState.None);
    permissionSet.AddPermission(
                        new SecurityPermission(SecurityPermissionFlag.Execution));
    condition = new AllMembershipCondition();
    statement = new PolicyStatement(permissionSet,
                                    PolicyStatementAttribute.Nothing);
    UnionCodeGroup codeGroup = new UnionCodeGroup(condition, statement);
    NamedPermissionSet localIntranet = FindNamedPermissionSet("LocalIntranet");
    condition = new ZoneMembershipCondition(SecurityZone.MyComputer);
    statement = new PolicyStatement(localIntranet,
                                    PolicyStatementAttribute.Nothing);

The following code restricts all code on this machine to the Local Intranet permissions when running within this AppDomain.

    UnionCodeGroup virtualIntranet = new UnionCodeGroup(condition, statement);
    virtualIntranet.Name = "Virtual Intranet";

Add the code group to the policy level.

    codeGroup.AddChild(virtualIntranet);

The root code group combines all permissions of its children.

    policyLevel.RootCodeGroup = codeGroup;

Set the new policy level of the temporary AppDomain.

    appDomain.SetAppDomainPolicy(policyLevel);
}

The following method is used to locate a named permission set within Windows. In this example, we use it to locate the Local Intranet permission set.

private NamedPermissionSet FindNamedPermissionSet(string name)
{
    IEnumerator policyEnumerator = SecurityManager.PolicyHierarchy();
    while (policyEnumerator.MoveNext())
    {
        PolicyLevel currentLevel = (PolicyLevel)policyEnumerator.Current;
        if (currentLevel.Label == "Machine")
        {
            IList namedPermissions = currentLevel.NamedPermissionSets;
            IEnumerator namedPermission = namedPermissions.GetEnumerator();
            while (namedPermission.MoveNext())
            {
                if (((NamedPermissionSet)namedPermission.Current).Name == name)
                {
                     return ((NamedPermissionSet)namedPermission.Current);
                }
            }
        }
    }
    return null;
}

Note

The default settings for Local Intranet disable runtime compilation of source code within the temporary AppDomain enforcing it. Runtime code compilation can open the door to malicious scripts, so it is recommended that you only support this feature in a trusted environment.

Conclusion

This chapter covered the implementation of an architecture that supports plugins loaded from the file system. Additional features, such as reloading the plugins when the plugin directory is modified, and the ability to dynamically compile source code in the plugin directory at runtime, were covered. There are definitely areas that could be improved, including the security section. Code access security could be used, for example, to deny file system access from the plugins. You could also sign each plugin with a common strong name key, and demand that linked plugins contain that public key. This would prevent malicious attempts to drop an unknown plugin into the plugin directory and execute it.

Another modification could be having a class that represents a proxy to an individual function within a plugin library. This way, you get improved caching instead of just caching the supported libraries and then finding the appropriate function each time you need to invoke it.

The Companion Web site contains the full source code to the plugin system, including a solid example showing the plugin system in action.

Figure 38.2 shows the main screen of the provided example.

Screenshot of the main interface for the provided example.

Figure 38.2. Screenshot of the main interface for the provided example.

There is also an integrated debug tool that will display the list of assemblies loaded in the main AppDomain. This was of great use when testing my system to make sure that plugins were not leaking into the main AppDomain.

Figure 38.3 shows the debug tool in action.

Screenshot of the debug tool in action.

Figure 38.3. Screenshot of the debug tool in action.

I advise you to use a similar technique when building your system so that you can be sure of the same thing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.108.119