Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

PluginManager

0.00/5 (No votes)
19 Jan 2003 8  
PluginManager: plug-in automation

What is this?

PluginManager is an automated plug-in handling system. It consists of 2 parts, namely the Proxy/Interface generator (which is responsible for making wrapper proxy classes and interfaces) and the PluginManager (which is responsible for loading/unloading the plug-ins and the AppDomain).

Why do I need this?

Suppose you have a persistent object that cannot destroyed (e.g. a network connection), but you also need provide loadable and unloadable plug-ins that will consume events and invoke methods from the persistent object. Finally, this will all need to run in the SAME program. This is an important consideration as allowing more than one process, the implantation could be easily done with a webservice and making consumer "plug-ins" for it. BUT we don't want that, do we?

Proxy/Interface generator

You will use this application to create a proxy classes and interfaces for your desired object to be consumed. Please note that this object has to inherit from MarshalByRefObject. Just one click of a button and a new assembly will be compiled and be ready to use. For the demo, the source of the output looks like:

namespace leppie.Plugins.Interface
{  
   public class PluginController : leppie.Plugins.Manager.BaseController
   {     
      public PluginController(leppie.Plugins.ChatSimulator arg0) : 
            base(arg0)
      {
      }
   }
   
   public class ChatSimulatorProxy : System.MarshalByRefObject, 
                   leppie.Plugins.Manager.IProxy, IChatSimulator
   {
      private leppie.Plugins.ChatSimulator _obj;
      
      public ChatSimulatorProxy(leppie.Plugins.ChatSimulator obj)
      {
         this._obj = obj;
         System.Reflection.EventInfo[] events = 
                   this._obj.GetType().GetEvents();
         for (int i = 0; (events.Length > i); i = (i + 1))
         {
            System.Reflection.EventInfo evt = events[i];
            evt.AddEventHandler(_obj, 
                System.Delegate.CreateDelegate(evt.EventHandlerType, 
                this, ("_" + evt.Name)));
         }
      }
      
      public event leppie.Plugins.MessageEventHandler Message;
      
      public override object InitializeLifetimeService()
      {
         return null;
      }
      
      public void Close()
      {
         System.Reflection.EventInfo[] events = 
                   this._obj.GetType().GetEvents();
         for (int i = 0; (events.Length > i); i = (i + 1))
         {
            System.Reflection.EventInfo evt = events[i];
            evt.RemoveEventHandler(_obj, 
               System.Delegate.CreateDelegate(evt.EventHandlerType, 
               this, ("_" + evt.Name)));
         }
      }
      
      public void _Message(object sender, leppie.Plugins.MessageEventArgs e)
      {
         try
         {
            if ((this.Message != null))
            {
               this.Message(sender, e);
            }
         }
         catch (System.AppDomainUnloadedException ex)
         {
         }
      }
      
      public void Connect()
      {
         this._obj.Connect();
      }
      
      public void Disconnect()
      {
         this._obj.Disconnect();
      }
      
      public void SendMessage(string user, string message)
      {
         this._obj.SendMessage( user,  message);
      }
   }
   
   public interface IChatSimulator
   {     
      event leppie.Plugins.MessageEventHandler Message;
      
      void Connect();
      
      void Disconnect();
      
      void SendMessage(string user, string message);
   }
   
   public interface IPlugin : leppie.Plugins.Manager.IBasePlugin
   {     
      void Load(IChatSimulator arg0);
   }
}

This is all that is required to use PluginManager. Most of the code in the proxy/interface maker is just boring CodeDom and reflection. I have tried to make it output VB.NET and associated assemblies, but unfortunately my VB.NET knowledge is NULL. For now, you can just consume the generated assembly (which will work from VB.NET).

Implementation of the generated assembly

In the main application you will need to create an instance of the generated PluginController class. This class allows for the loading/unloading of the "Plug-ins" AppDomain. This in turn calls the PluginManager that takes care of the rest. Example:

chat = new ChatSimulator();
pc = new PluginController(chat);

That is it!

Implementation of the plug-in

There are 2 rules for making a plug-in. Firstly, it must have the Plugin attribute applied to the plug-in class. Secondly, you will need to implement the IPlugin interface (which has 2 methods Load and Unload from IBasePlugin). Note how, interface that was created allows you to just make one change to an existing "fixed" plug-in (replace ChatSimulator reference with an IChatSimulator reference). Example:

[Plugin]
public class TestPlugin : IPlugin
{
   private IChatSimulator chat;

   public void Load(IChatSimulator chat)
   {
      Console.WriteLine("Loading plugin: " + this);
      this.chat = chat;
      this.chat.Message += new MessageEventHandler(MessageReceived);
   }

   private void MessageReceived(object sender, MessageEventArgs e)
   {
      if (e.User == "Console")
      {
         Console.WriteLine("Event in: {0}, MSG: {1}, USER: {2}, SENDER: {3}",
            this.GetType().Name, e.Message, e.User, sender.GetType().Name);
         chat.SendMessage(this.GetType().Name, "pong: " + e.Message);
      }
   }

   public void Unload()
   {
      this.chat.Message -= new MessageEventHandler(MessageReceived);
      Console.WriteLine("Unloading plugin: " + this);
   }
}

Be sure to register event listeners in Load and deregister them in the Unload method (although not strictly required).

Points of interest

While doing this I was referred to the SOAPsuds tool. Unfortunately, this tool does not create the correct code I need, thus the need to make custom generator. The problem is that events and delegates are just wrapped, but not relayed. Thus in my case (running in one process) this will STILL load the plug-in assembly, defeating the point of isolation.

While the remoting part of this is totally transparent, its in fact being used. For example: Passing a MarshalByRefObject-derived class as a parameter to an object's method in another AppDomain will automagically (yes, that is correct) convert the passed object to an ObjRef. This can be seen in the debugger.

Another problem aroused to create an object in another AppDomain. You will need to use an ObjectHandle, that is for use with MarshalByObjectValue classes. This was in fact the confusing part as this is the ONLY way to instantiate an object (whether by value or by reference) in another AppDomain. I wish there were more remoting examples in MSDN.

I have also chosen to load the plug-in assemblies via memory, meaning that the plug-in assemblies can be deleted/overwritten. Thus making it easy to attach the debugger to the process and then build new versions on the plug-ins via the "Debug > Start new instance" right click menu. All that's required is to reload the PluginManager via the PluginController or set PluginController.EnableFileSystemWatcher to true. NOTE: make sure the plug-in loads and behaves before doing this.

Finally, some usage notes:

  • Look at the project configuration for the TestPlugin in the DemoApp. Please make the output of the plug-in go into a Plugins subdirectory in the DemoApp debug/release folder.
  • Set all non-GAC assembly references' CopyLocal property to false. THIS is important. The project will not build. Also add supported assemblies to the DemoApp debug/release folder.
  • Look out for exceptions that can be caused when serializing classes, particularly EventArgs type classes. Mark these classes as Serializable or inherit from MarshalByRefObject.

PluginController outline (for example)

public class PluginController : BaseController
{
  public PluginController(ChatSimulator arg0); 
  public void LoadPlugin(string filename);
  public void LoadPluginManager();
  public void UnloadPlugin(string typename);
  public void UnloadPluginManager();
  public bool EnableFileSystemWatcher { get; set; }
  public string[] PluginNames { get; } 
}

Thanks to Lutz Roeder's Reflector.

Conclusion

When running the DemoApp, note in the output window how the AppDomains are loaded/unloaded.

My personal implementation of this is for the Sharkbite IRC library (with some slight modification). I had it running the weekend without any problems on working plug-ins. This actually passes 2 objects to the PluginController. So it seems to work for multiple exposed classes.

Known problems/warnings

  • Unloading/reloading from a proxied event is not recommended and will throw an exception that is impossible to catch, except from the class being proxied (i.o.w. the event invoker). See CL code for more details. I recommend using the FileSystemWatcher.
  • Classes that are being proxied are done so at "root" level, only so interfaces are not generated for contained classes. If you need to use events from these, add the class to be proxied as well, or perhaps re-think what classes are being exposed to the plug-in. In my case with the Sharkbite IRC library, I had a single Connection class with 2 containing classes (Sender, Listener). It was in my case better to pass both a Sender and a Listener object into the plug-in. Anyone interested in a copy of the Sharkbite implementation can E-mail me.

Updates

  • (01/20) - Added a WinDemoApp to example plug-in
  • (01/20) - Added release compiled WinDemoApp/DemoApp (TIP: for a nice demo, run the app (Win or CL) and delete the plug-in, then restore it from the Recycle Bin, you will see how it gets loaded/unloaded).
  • (01/21) - Updated the source with more comments and event now uses the .NET style (object, EventArgs) events.
  • (01/21) - Added some extra details throughout the article and fixed dumb IntelliSense-less (read class names) errors.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here