Windows Service Auto-Update Plugin Framework

Introduction

Windows services are long running processes that operate in the background, however, just because they are out of the way does not mean they don't need to be updated. One of the problems with services is that they need admin access to install and reinstall when code needs to be updated. This article describes a framework that allows you to update the code in your windows service on demand, without the need for admin intervention. The code given works and a more integrated version is in production - the article here presents the general overview. I have it on my personal backlog to update the code into a proper framework hopefully in Q1/2 of 2017 ... feel free to annoy me if you need it and it's not done by then :)

There are many tweaks and different approaches that could be taken to implement the detail, depending on your particular requirements. Hopefully this will give your particular solution a head-start. I encourage you to download the code, and if you have any improvements please leave a comment and send them on so everyone can benefit. The methodology presented here is very rough at the moment and leaves a lot of internal adaptation up to the developer. In a future release I will wrap the code together into a more robust self managing framework that can be installed as a package, that includes some other interesting stuff I am working on at the moment!



Background

If you have a Windows service on a handful of sites and need to update it, it's not a problem ... dial-in, hit the command line and the job is done. When you have a large installed user-base however things start to get a little bit more problematic. The framework design discussed here was put together to serve as the basis of a large scale self managing / remote updating eco-system of machines. The main concepts are introduced here with suggestions for your own implementation.

Nutshell

The main concept behind the framework is to remove all work from the service itself, and use it only as a shell to load/unload plugin assemblies that carry out any required work. This is achieved by loading the assemblies into one or more application domains that critically, are separate from the main service host application domain. The reason for the separate domains, is that while you can easily load a plugin into the current/main app-domain, you can only unload an entire domain at once, you cannot be more specific than that. If we have our plugins loaded into the same domain as the core service application, then unloading the plugins, by default, unloads the main application as well. In this framework implementation, the service host only needs to know two things - when, and how to load/unload plugins. Everything else is handled by a plugin host controller, and the plugins themselves.


Operation

The framework operates as follows.

Setup

The service can do two things (1) create a plugin controller and keep it at arms length using MarshalByRef, (2) receive event messages sent to it by the plugin controller.

Managing

The plugin controller creates 1..n application domains as needed. In the case of this demo I created a "command" domain and one called "plugins". The concept is that "command" might be used to check against a web-service for updated versions of plugins and use that to kick off a "refresh / reload" routine, and the "plugins" carry out some worker processes. Command plugins typically would encompass a scheduler object that triggers actions at certain time intervals.


Messaging

The framework is controlled by messages that flow from plugins, to the controller and up to the host service program. Messages can be simple log and notification messages, or may be action messages that tell either the controller or the service to trigger a particular action. Trigger actions could be commands like "check for new version on server", "ping home to main server", "load/unload a particular app domain". As the objective is to keep all work and logic away from the service, take care to separate work into discrete plugin packages. Not all plugins need to be for loaded all the time consuming resources. By using different application domains you can facilitate load/unload on demand using a main scheduler plugin.

Plugin definition

With any plugin system an important part building block is a known interface definition that the plugin controller can manage. To kick things off, I created an interface that encompasses the minimum functionality I required. This included methods to flag a running process that it is to stop, and signal a self-unload event, when it completes its process run

  1. // Interface each plugin must implement  
  2.     public interface IPlugin  
  3.     {  
  4.         string PluginID(); // this should be a unique GUID for the plugin - a different one may be used for each version of the plugin.  
  5.         bool TerminateRequestReceived(); // internal flag if self-terminate request has been received  
  6.         string GetName(); // assembly friendly name  
  7.         string GetVersion();// can be used to store verison of assembly  
  8.         bool Start();// trigger assembly to start  
  9.         bool Stop(); // trigger assembly to stop  
  10.         void LogError(string Message, EventLogEntryType LogType); // failsafe - logs to eventlog on error  
  11.         string RunProcess(); // main process that gets called  
  12.         void Call_Die(); // process that gets called to kill the current plugin  
  13.         void ProcessEnded(); // gets called when main process ends, ie: web-scrape complete, etc...  
  14.   
  15.         // custom event handler to be implemented, event arguments defined in child class  
  16.         event EventHandler<plugineventargs> CallbackEvent;  
  17.         PluginStatus GetStatus(); // current plugin status (running, stopped, processing...)  
  18.     }  
  19.    
  20. </plugineventargs>  
When we send messages over a remoting boundary, we need to serialize the messages. For this implementation I chose to create a custom EventArgs class to send with my event messages. 
  1. // event arguments defined, usage: ResultMessage is for any error trapping messages, result bool is fail/success  
  2.  // "MessageType" used to tell plugin parent if it needs to record a message or take an action etc.  
  3.  [Serializable]  
  4.  public class PluginEventArgs : EventArgs  
  5.  {  
  6.      public PluginEventMessageType MessageType;  
  7.      public string ResultMessage;  
  8.      public bool ResultValue;  
  9.      public string MessageID;  
  10.      public string executingDomain;  
  11.      public string pluginName;  
  12.      public string pluginID;  
  13.      public PluginEventAction EventAction;  
  14.      public CallbackEventType CallbackType;   
  15.   
  16.      public PluginEventArgs(PluginEventMessageType messageType = PluginEventMessageType.Message, string resultMessage = "",PluginEventAction eventAction = (new PluginEventAction()), bool resultValue = true)  
  17.      {  
  18.          // default empty values allows us to send back default event response  
  19.          this.MessageType = messageType; // define message type that is bring sent  
  20.          this.ResultMessage = resultMessage; // used to send any string messages  
  21.          this.ResultValue = resultValue;  
  22.          this.EventAction = eventAction; // if the event type = "Action" then this carries the action to take  
  23.          this.executingDomain = AppDomain.CurrentDomain.FriendlyName;  
  24.          this.pluginName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;  
  25.          //this.pluginID = ((IPlugin)System.Reflection.Assembly.GetExecutingAssembly()).PluginID();  
  26.      }  
  27.  }  

There are a number of supporting types and classes as you can see - I don't wish to copy the entire code into the article so if you wish to see the details please download the attached code and go through it in Visual Studio.

Plugin manager

The plugin manager contains two main classes, the PluginHost, and the Controller. All are wrapped as remote objects using MarshalByRefObject.

Plugin host

The host keeps the controller and plugins an arm's length away from the main application. It defines and sets up the different app-domains, then calls the controller to load and manage the plugins themselves. 

  1.    public class PluginHost : MarshalByRefObject  
  2.     {  
  3.         private const string DOMAIN_NAME_COMMAND = "DOM_COMMAND";  
  4.         private const string DOMAIN_NAME_PLUGINS = "DOM_PLUGINS";  
  5.   
  6.         private AppDomain domainCommand;  
  7.         private AppDomain domainPlugins;  
  8.           
  9.         private PluginController controller_command;  
  10.         private PluginController controller_plugin;  
  11.   
  12.         public event EventHandler<plugineventargs> PluginCallback;  
  13.         ...  
  14.  
Loading into a domain. 
  1. public void LoadDomain(PluginAssemblyType controllerToLoad)  
  2. {  
  3.     init();  
  4.     switch (controllerToLoad)  
  5.     {  
  6.         case PluginAssemblyType.Command:  
  7.             {  
  8.                 controller_command = (PluginController)domainCommand.CreateInstanceAndUnwrap((typeof(PluginController)).Assembly.FullName, (typeof(PluginController)).FullName);  
  9.                 controller_command.Callback += Plugins_Callback;  
  10.                 controller_command.LoadPlugin(PluginAssemblyType.Command);  
  11.                 return;  
  12.             }  
  13.         case PluginAssemblyType.Plugin:  
  14.             {  
  15.                 controller_plugin = (PluginController)domainPlugins.CreateInstanceAndUnwrap((typeof(PluginController)).Assembly.FullName, (typeof(PluginController)).FullName);  
  16.                 controller_plugin.Callback += Plugins_Callback;  
  17.                 controller_plugin.LoadPlugin(PluginAssemblyType.Plugin);  
  18.                 return;  
  19.             }  
  20.     }  
  21. }  

Plugin controller

The plugin controller is closest to the plugins themselves. It is the first port of call for the message flow, and takes care of controlling message flow between plugins, and from the plugins back up to the service application program.

  1. void OnCallback(PluginEventArgs e)  
  2. {  
  3.     // raise own callback to be hooked by service/application  
  4.     // pass through callback messages received if relevant  
  5.     if (e.MessageType == PluginEventMessageType.Action)  
  6.     {  
  7.    ....  
  8.         else if (e.EventAction.ActionToTake == PluginActionType.Unload) // since the plugin manager manages plugins, we intercept this type of message and dont pass it on  
  9.         {  
  10.    ....  
  11.     else  
  12.     {  
  13.         if (Callback != null// should ONLY happen is not type action and only message  
  14.         {  
  15.             Callback(this, e);  
  16.         }  
  17.     }  

Plugins

For this demo example, the plugins are being kept very simple. All but one has the same code. They have a timer, and onInterval prints a message to the console. If they receive a shutdown message, they shut-down immediately, unless they are in the middle of a process, in which case they will complete that process and then signal they are ready for unloading.

  1. public bool Stop()  
  2.         {  
  3.             if (_Status == PluginStatus.Running) // process running - cannot die yet, instead, flag to die at next opportunity  
  4.             {  
  5.                 _terminateRequestReceived = true;  
  6.                 DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Stop called but process is running from: " + _pluginName));   
  7.             }  
  8.             else  
  9.             {  
  10.                 if (counter != null)  
  11.                 {  
  12.                     counter.Stop();  
  13.                 }  
  14.                 _terminateRequestReceived = true;  
  15.                 DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Stop called from: " + _pluginName));  
  16.                 Call_Die();  
  17.             }  
  18.               
  19.             return true;  
  20.         }  
  21.   
  22. ...  
  23.   
  24.         // OnTimer event, process start raised, sleep to simulate doing some work, then process end raised  
  25.         public void OnCounterElapsed(Object sender, EventArgs e)  
  26.         {  
  27.             _Status = PluginStatus.Processing;  
  28.             DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Counter elapsed from: " + _pluginName));  
  29.             if (_terminateRequestReceived)  
  30.             {  
  31.                 counter.Stop();  
  32.                 DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Acting on terminate signal: " + _pluginName));  
  33.                 _Status = PluginStatus.Stopped;  
  34.                 Call_Die();  
  35.             }  
  36.             else  
  37.             {  
  38.                 _Status = PluginStatus.Running; // nb: in normal plugin, this gets set after all processes complete - may be after scrapes etc.  
  39.             }  
  40.         }  
The "command / control" plugin simulates requesting the service update itself (hey, finally, the reason we came to this party!) .... 
  1. // OnTimer event, process start raised, sleep to simulate doing some work, then process end raised  
  2. public void OnCounterElapsed(Object sender, EventArgs e)  
  3. {  
  4.     _Status = PluginStatus.Processing;  
  5.     DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Counter elapsed from: " + _pluginName));  
  6.     if (_terminateRequestReceived)  
  7.     {  
  8.         DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Counter elapsed, terminate received, stopping process...  from: " + _pluginName));  
  9.     }  
  10.   
  11.     // TEST FOR DIE...  
  12.     DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND ***"));  
  13.     PluginEventAction actionCommand = new PluginEventAction();  
  14.     actionCommand.ActionToTake = PluginActionType.TerminateAndUnloadPlugins; // TEST !!!! ... this should ONLY be used to signal the HOST/CONTROLLER to flag a DIE....  
  15.     DoCallback(new PluginEventArgs(PluginEventMessageType.Action, null, actionCommand));  
  16.     DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND - COMPLETE ***"));  
  17.     Call_Die();  
  18.     // end test  
  19. }  
A critical "gotcha" snippet of code overrides the MarshalByRef "InitializeLifetimeService" method. By default, a remote object will die after a short space of time. By overriding this you ensure your object stays live as long as you wish. 
  1. public override object InitializeLifetimeService()  
  2. {  
  3.     return null;  
  4. }  

Service program

When we start the service, we hook the plugin manager event callback.

  1. public void Start()  
  2. {  
  3.     if (pluginHost == null)  
  4.     {  
  5.         pluginHost = new PluginHost();  
  6.         pluginHost.PluginCallback += Plugins_Callback;  
  7.         pluginHost.LoadAllDomains();  
  8.         pluginHost.StartAllPlugins();  
  9.     }  
  10. }  
When an unload event bubbles up, we can shell out to an MSI installer that we run in silent mode, and use it to update the plugins themselves. The MSI installer is simply a means of wrapping things nicely in a package. The objective is to run the msi in silent mode, therefore requiring no user interaction. You could also use nuget etc and I will investigate this in a further iteration. 
  1. private void Plugins_Callback(object source, PluginContract.PluginEventArgs e)  
  2. {  
  3.     if (e.MessageType == PluginEventMessageType.Message)  
  4.     {  
  5.         EventLogger.LogEvent(e.ResultMessage, EventLogEntryType.Information);  
  6.         Console.WriteLine(e.executingDomain + " - " + e.pluginName + " - " + e.ResultMessage); // for debug  
  7.     }  
  8.     else if (e.MessageType == PluginEventMessageType.Action) {  
  9.         if (e.EventAction.ActionToTake == PluginActionType.UpdateWithInstaller)  
  10.         {  
  11.             Console.WriteLine("****  DIE DIE DIE!!!! ... all plugins should be DEAD and UNLOADED at this stage ****");  
  12.             EventLogger.LogEvent("Update with installer event received", EventLogEntryType.Information);  
  13.             // Plugin manager takes care of shutting things down before calling update so we are safe to proceed...  
  14.             if (UseInstallerVersion == 1)  
  15.             {  
  16.                 EventLogger.LogEvent("Using installer 1", EventLogEntryType.Information);  
  17.                 UseInstallerVersion = 2;  
  18.                 // run installer1 in silent mode - it should replace files, and tell service to re-start  
  19.             }  
  20.             else if (UseInstallerVersion == 2)  
  21.             {  
  22.                 EventLogger.LogEvent("Using installer 2", EventLogEntryType.Information);  
  23.                 // run installer2 in silent mode - it should replace files, and tell service to re-start  
  24.                 UseInstallerVersion = 1;  
  25.             }  
  26.         }  
  27.     }  
  28. }  
Congratulations, you now have a self-updating Windows service that once installed, can be managed remotely with little or no intervention. Happy Service Coding!  :) 


Similar Articles