Introduction
Recently, I had to come up with a solution to a typical problem. Build "something" that has the ability to grow over time, can be easily maintained by future developers, and utilizes CPU resources effectively. Well, that said, I think most of everything we write should probably have these considerations. However, not all challenges are the same, so in this case, I dug up the CCR again; it's never failed me in the past, so why should it now?
I've written this article on the premise that "intermediate" developers will be reading this, therefore, I've left out a lot of code description simply for the reason that it would be unnecessary for me to describe each part. You should have a decent understanding of Predicates and Lambda to understand sections of this example. If you're a learn by example type, then you should probably be OK.
Background
The goal of the project was to create something similar to a WISE process. There had to be a "core" that has the ability to manage new code parts and allocate them appropriately without recompiling the core. Therefore, by utilizing "plugin" techniques and coordinating their execution via the CCR, this could be accomplished quite easily.
The following article deals with the conceptual parts in their basic form, stripped down from the original project in order to demonstrate an effective solution.
Using the Code
I wanted to start by reviewing the sections of the solution.
In order to create plug-ins, we'll need two main things. First and foremost is our interface so we can identify the loaded DLLs we find in out Plugin directory. Secondly, we'll create some event arguments to help us determine whether or not our plug-in failed or succeeded.
Make note, in the IPlugin
interface, we have to implement an Event (ProcessComplete
) used to "callback" to somewhere to inform us the plug-in has exited; not really necessary for this demo; however, in production cases, we probably want to keep an audit trail of events in our application.
We also have our main method "BeginProcess
". This is what our "PluginMonitor
" will call the moment the monitor is called because of the availability of a new thread. You should probably not treat this as an initialization method or constructor since the "state" of the plug-in will be monitored immediately upon thread attachment.
The final method "Done
" should be used to dispose of any object and, if one exists, call the method connected to ProcessComplete
. As mentioned before, it's not necessary, but it's probably good practice.
Now, let's do a quick review of the main classes that will do all the work.
PluginProvider
: We need an object that will collect all the plug-ins for us. It's pretty straightforward, and can be extracted, modified, and used in anything you might want to incorporate this technique in. It assumes we have a folder called "Plugins" where we will drop all of our DLLs that have inherited the IPlugin
interface. PluginMonitor
: We will also need something we can wrap around the plug-in to monitor it on the allocated thread. We don't want to do this as part of the PluginProcessor
simply because we want to separate the objects based on their duties. PluginProcessor
: This is where we want to drop all the CCR logic; it's probably pretty similar to my previous article of creating a threaded sequential logging solution. I wanted to utilize the easiest implementation for this example to get you started, and I will probably revisit this in the future to advance this code a little more. Notice, we have another event "AddProcess
". I included this to help us keep track of which plug-ins were being executed simply for helping us understand and see what is happening during runtime.
A quick look at the common interface reveals something pretty standard. Later, I will create a test plug-in we can use to test the "plugin director". We'll simply just copy and paste multiple versions of the same plug-in in the plugins directory.
IPlugin
using System;
namespace Common
{
public interface IPlugin
{
event EventHandler ProcessComplete;
string PluginName { get; set; }
string PluginDescription { get; set; }
int PercentComplete { get; set; }
void BeginProcess();
void Done(PluginEventArgs evenArgs);
}
public class PluginEventArgs : EventArgs
{
public bool Success { get; set; }
public Exception Error { get; set; }
}
}
Now, let's take a look at the PluginProvider
. This object simply loads all the appropriate DLLs in the Plugins directory and returns a List
of type IPlugin
.
PluginProvider
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Common;
namespace Director
{
public static class PluginProvider
{
public static List<IPlugin> GetPlugins()
{
return FindPlugIns(LoadPlugInAssemblies());
}
private static List<Assembly> LoadPlugInAssemblies()
{
var dInfo = new DirectoryInfo(Path.Combine(
Environment.CurrentDirectory, "Plugins"));
var files = dInfo.GetFiles("*.dll");
var plugInAssemblyList = new List<Assembly>();
foreach (var file in files)
plugInAssemblyList.Add(Assembly.LoadFile(file.FullName));
return plugInAssemblyList;
}
private static List<IPlugin> FindPlugIns(IEnumerable<Assembly> assemblies)
{
var availableTypes = new List<Type>();
foreach (var currentAssembly in assemblies)
availableTypes.AddRange(currentAssembly.GetTypes());
var pluginList = availableTypes.FindAll(t =>
new List<Type>(t.GetInterfaces()).Contains(typeof(IPlugin)));
return pluginList.ConvertAll(t => Activator.CreateInstance(t) as IPlugin);
}
}
}
For those not familiar with Lambda, here's an example of how it can be utilized. In some ways, it reduces readability, but it sure does cut down on the amount of typing you do! :)
First, we use "FindAll
" to gather all of the "IPlugin
" types, then we use ConvertAll
to return the assembly objects as IPlugin
types.
var pluginList = availableTypes.FindAll(t =>
new List<Type>(t.GetInterfaces()).Contains(typeof(IPlugin)));
return pluginList.ConvertAll(t => Activator.CreateInstance(t) as IPlugin);
And, the final piece of the core "Director" modules is the PluginProcessor
. This section deals strictly with the CCR. Please review my previous article on the CCR to help understand the following objects.
Please note, in this example, I am not using "background threading" (Dispatcher
object); therefore, you'll need to call the Dispose
method in order to exit your application properly.
If you wish to keep track of the initiated plug-ins, make sure to hookup "AddProcess
".
PluginProcessor
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Ccr.Core;
namespace Director
{
public class PluginProcessor : IDisposable
{
private static readonly Dispatcher MainDispatcher =
new Dispatcher(2, ThreadPriority.Normal, false, "ProcessPool");
private static DispatcherQueue _mainDispatcherQueue;
private static readonly Port<PluginMonitor> MainProcessPort =
new Port<PluginMonitor>();
private bool _disposed;
public delegate void ProcessDelegate(PluginMonitor processMonitor);
public event ProcessDelegate AddProcess;
public PluginProcessor(IEnumerable<PluginMonitor> processes)
{
foreach (var process in processes)
MainProcessPort.Post(process);
_mainDispatcherQueue = new DispatcherQueue(
"MainDispatcherQueue", MainDispatcher);
Arbiter.Activate(_mainDispatcherQueue,
Arbiter.Receive(true, MainProcessPort, StartProcess));
}
private void StartProcess(PluginMonitor pluginMonitor)
{
if (AddProcess != null)
AddProcess(pluginMonitor);
pluginMonitor.BeginProcess();
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
MainDispatcher.Dispose();
_mainDispatcherQueue.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Now, all we need is to create a plug-in for ourselves. The following code basically does nothing important, but it emulates output and progress at different intervals. I feel a little obligated to explain some parts of this code simply because some parts just seem silly, but, they do have a true reason. At the beginning of this plug-in, you'll notice something like this:
_pluginID = Convert.ToUInt16(Guid.NewGuid().ToString()[0]);
_random = new Random(_pluginID);
The reason this is there is to help create a truly new "seed" value for the randomizer, simply because you'll end up having a number of plug-ins completing at the same time. Not like it's a big deal, but it's good to see the results of a variable "alive" time for the plug-in. The following code is a complete implementation of an IPlugin
inherited class. We just need to take the compiled DLL from the bin and drop it in the Plugins directory of the main application before we execute it. As I mentioned, it really does nothing, it simply creates a variable random length timer to simulate the complete processing time. It changes the description state field to simulate something happening. All for show.
using System;
using System.Threading;
using Common;
namespace MyPlugin
{
class TestPlugin : IPlugin
{
public event EventHandler ProcessComplete;
private readonly int _pluginID;
private readonly Random _random;
private int _completed;
private Timer _tmr;
public TestPlugin()
{
_pluginID = Convert.ToUInt16(Guid.NewGuid().ToString()[0]);
_random = new Random(_pluginID);
}
public void BeginProcess()
{
_tmr = new Timer(Counter, null, 0, _random.Next(5, 20));
}
public string PluginName
{
get { return String.Format("Plugin ID : {0}", _pluginID); }
set{}
}
public int PercentComplete
{
get { return _completed; }
set {}
}
public string PluginDescription { get; set; }
private void Counter(object sender)
{
_completed += 1;
switch (_completed)
{
case 1:
PluginDescription = "Initializing plugin.";
break;
case 20:
PluginDescription = "Gathering plugin information.";
break;
case 40:
PluginDescription = "Examining registry";
break;
case 60:
PluginDescription = "Examining files.";
break;
case 80:
PluginDescription = "Compiling restore point.";
break;
case 90:
PluginDescription = "Cleaning up temporary files.";
break;
}
if (_completed < 100) return;
_tmr.Dispose();
Done(new PluginEventArgs { Error = null, Success = true });
}
public void Done(PluginEventArgs evenArgs)
{
if (ProcessComplete != null)
ProcessComplete(this, evenArgs);
}
}
}
And finally, the implementation of the core to some application. In this case, I've created a small console application. First, we call the Provider to give us a list of eligible plug-in objects. We need to create a List
of <PluginMonitor>
by passing off each "plug-in" to a monitor.
var processMonitors = new List<PluginMonitor>();
var plugins = PluginProvider.GetPlugins();
foreach (var p in plugins)
processMonitors.Add(new PluginMonitor(p));
Once that is complete, we need to pass all of these ProcessMonitor
s to the PluginProcessor
; it will invoke the CCR and handle all the threading. We also want to hook up the "AddProcess
" so we can watch each plug-in execute.
_processor = new PluginProcessor(processMonitors);
_processor.AddProcess += PluginMonitorAddProcess;
Here is the full listing of the implementation:
using System;
using System.Collections.Generic;
using Director;
namespace Interface
{
class Program
{
static void Main(string[] args)
{
new MainApp();
}
}
public class MainApp
{
private readonly PluginProcessor _processor;
public MainApp()
{
var processMonitors = new List<PluginMonitor>();
var plugins = PluginProvider.GetPlugins();
foreach (var p in plugins)
processMonitors.Add(new PluginMonitor(p));
_processor = new PluginProcessor(processMonitors);
_processor.AddProcess += PluginMonitorAddProcess;
}
static void PluginMonitorAddProcess(PluginMonitor pluginMonitor)
{
Console.WriteLine(string.Format("{0} plugin started.",
pluginMonitor.PluginName));
}
}
}
Running the Code Example
After you compile the application, you need to copy your DLL from the TestPlugin into the folder called Plugins in the same directory the main EXE is located. If the folder does not exit, create it.
You should get something similar to this:
You'll need to review the code to determine the meaning of the output. In general, it is showing, among other things, the current "state and percentage complete" of each plug-in. Graphically, this process looks much nicer, but, hopefully you can take this to the next level.
I hope this article helps you think of new innovations, or sparks your creative side to improve past, present, and future code projects!
Points of Interest
You may want to play with the "Dispatcher
" object in the PluginProcessor
to get a better understanding of the distribution of processes. Try updating the IPlugin
interface to incorporate things like dependency and sorting for execution.
Happy CCRing!