Click here to Skip to main content
Click here to Skip to main content

Windows service auto-update plugin framework

, 31 Dec 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
Using a plugin system to auto-update functionality of a windows service without the need to re-install the service

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. 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, its not a problem ... dial-in, hit the command line and job 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 keeps 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 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  


    // Interface each plugin must implement
    public interface IPlugin
    {
        string PluginID(); // this should be a unique GUID for the plugin - a different one may be used for each version of the plugin.
        bool TerminateRequestReceived(); // internal flag if self-terminate request has been received
        string GetName(); // assembly friendly name
        string GetVersion();// can be used to store verison of assembly
        bool Start();// trigger assembly to start
        bool Stop(); // trigger assembly to stop
        void LogError(string Message, EventLogEntryType LogType); // failsafe - logs to eventlog on error
        string RunProcess(); // main process that gets called
        void Call_Die(); // process that gets called to kill the current plugin
        void ProcessEnded(); // gets called when main process ends, ie: web-scrape complete, etc...

        // custom event handler to be implemented, event arguments defined in child class
        event EventHandler<plugineventargs> CallbackEvent;
        PluginStatus GetStatus(); // current plugin status (running, stopped, processing...)
    }
 
</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. 

 
    // event arguments defined, usage: ResultMessage is for any error trapping messages, result bool is fail/success
    // "MessageType" used to tell plugin parent if it needs to record a message or take an action etc.
    [Serializable]
    public class PluginEventArgs : EventArgs
    {
        public PluginEventMessageType MessageType;
        public string ResultMessage;
        public bool ResultValue;
        public string MessageID;
        public string executingDomain;
        public string pluginName;
        public string pluginID;
        public PluginEventAction EventAction;
        public CallbackEventType CallbackType; 
 
        public PluginEventArgs(PluginEventMessageType messageType = PluginEventMessageType.Message, string resultMessage = "",PluginEventAction eventAction = (new PluginEventAction()), bool resultValue = true)
        {
            // default empty values allows us to send back default event response
            this.MessageType = messageType; // define message type that is bring sent
            this.ResultMessage = resultMessage; // used to send any string messages
            this.ResultValue = resultValue;
            this.EventAction = eventAction; // if the event type = "Action" then this carries the action to take
            this.executingDomain = AppDomain.CurrentDomain.FriendlyName;
            this.pluginName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
            //this.pluginID = ((IPlugin)System.Reflection.Assembly.GetExecutingAssembly()).PluginID();
        }
    }
 

 

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 detail 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 arms 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.

    public class PluginHost : MarshalByRefObject
    {
        private const string DOMAIN_NAME_COMMAND = "DOM_COMMAND";
        private const string DOMAIN_NAME_PLUGINS = "DOM_PLUGINS";

        private AppDomain domainCommand;
        private AppDomain domainPlugins;
        
        private PluginController controller_command;
        private PluginController controller_plugin;

        public event EventHandler<plugineventargs> PluginCallback;
        ...
</plugineventargs>

Loading into a domain

        public void LoadDomain(PluginAssemblyType controllerToLoad)
        {
            init();
            switch (controllerToLoad)
            {
                case PluginAssemblyType.Command:
                    {
                        controller_command = (PluginController)domainCommand.CreateInstanceAndUnwrap((typeof(PluginController)).Assembly.FullName, (typeof(PluginController)).FullName);
                        controller_command.Callback += Plugins_Callback;
                        controller_command.LoadPlugin(PluginAssemblyType.Command);
                        return;
                    }
                case PluginAssemblyType.Plugin:
                    {
                        controller_plugin = (PluginController)domainPlugins.CreateInstanceAndUnwrap((typeof(PluginController)).Assembly.FullName, (typeof(PluginController)).FullName);
                        controller_plugin.Callback += Plugins_Callback;
                        controller_plugin.LoadPlugin(PluginAssemblyType.Plugin);
                        return;
                    }
            }
        }
...

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.

        void OnCallback(PluginEventArgs e)
        {
            // raise own callback to be hooked by service/application
            // pass through callback messages received if relevant
            if (e.MessageType == PluginEventMessageType.Action)
            {
           ....
                else if (e.EventAction.ActionToTake == PluginActionType.Unload) // since the plugin manager manages plugins, we intercept this type of message and dont pass it on
                {
           ....
            else
            {
                if (Callback != null) // should ONLY happen is not type action and only message
                {
                    Callback(this, e);
                }
            }

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, print 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.

        public bool Stop()
        {
            if (_Status == PluginStatus.Running) // process running - cannot die yet, instead, flag to die at next opportunity
            {
                _terminateRequestReceived = true;
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Stop called but process is running from: " + _pluginName)); 
            }
            else
            {
                if (counter != null)
                {
                    counter.Stop();
                }
                _terminateRequestReceived = true;
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Stop called from: " + _pluginName));
                Call_Die();
            }
            
            return true;
        }

...

        // OnTimer event, process start raised, sleep to simulate doing some work, then process end raised
        public void OnCounterElapsed(Object sender, EventArgs e)
        {
            _Status = PluginStatus.Processing;
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Counter elapsed from: " + _pluginName));
            if (_terminateRequestReceived)
            {
                counter.Stop();
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Acting on terminate signal: " + _pluginName));
                _Status = PluginStatus.Stopped;
                Call_Die();
            }
            else
            {
                _Status = PluginStatus.Running; // nb: in normal plugin, this gets set after all processes complete - may be after scrapes etc.
            }
        }

The "command / control" plugin simulates requesting that the service update itself (hey, finally, the reason we came to this party!) ....

        // OnTimer event, process start raised, sleep to simulate doing some work, then process end raised
        public void OnCounterElapsed(Object sender, EventArgs e)
        {
            _Status = PluginStatus.Processing;
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Counter elapsed from: " + _pluginName));
            if (_terminateRequestReceived)
            {
                DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "Counter elapsed, terminate received, stopping process...  from: " + _pluginName));
            }

            // TEST FOR DIE...
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND ***"));
            PluginEventAction actionCommand = new PluginEventAction();
            actionCommand.ActionToTake = PluginActionType.TerminateAndUnloadPlugins; // TEST !!!! ... this should ONLY be used to signal the HOST/CONTROLLER to flag a DIE....
            DoCallback(new PluginEventArgs(PluginEventMessageType.Action, null, actionCommand)); 
            DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND - COMPLETE ***"));
            Call_Die();
            // end test
        }

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.

    public override object InitializeLifetimeService()
    {
        return null;
    }


Service program

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

        public void Start()
        {
            if (pluginHost == null)
            { 
                pluginHost = new PluginHost();
                pluginHost.PluginCallback += Plugins_Callback;
                pluginHost.LoadAllDomains();
                pluginHost.StartAllPlugins();
            }
        }

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. 

        private void Plugins_Callback(object source, PluginContract.PluginEventArgs e)
        {
            if (e.MessageType == PluginEventMessageType.Message)
            {
                EventLogger.LogEvent(e.ResultMessage, EventLogEntryType.Information);
                Console.WriteLine(e.executingDomain + " - " + e.pluginName + " - " + e.ResultMessage); // for debug    
            }
            else if (e.MessageType == PluginEventMessageType.Action) {
                if (e.EventAction.ActionToTake == PluginActionType.UpdateWithInstaller)
                {
                    Console.WriteLine("****  DIE DIE DIE!!!! ... all plugins should be DEAD and UNLOADED at this stage ****");
                    EventLogger.LogEvent("Update with installer event received", EventLogEntryType.Information);
                    // Plugin manager takes care of shutting things down before calling update so we are safe to proceed...
                    if (UseInstallerVersion == 1)
                    {
                        EventLogger.LogEvent("Using installer 1", EventLogEntryType.Information);
                        UseInstallerVersion = 2;
                        // run installer1 in silent mode - it should replace files, and tell service to re-start
                    }
                    else if (UseInstallerVersion == 2)
                    {
                        EventLogger.LogEvent("Using installer 2", EventLogEntryType.Information);
                        // run installer2 in silent mode - it should replace files, and tell service to re-start
                        UseInstallerVersion = 1;
                    }
                }
            }
        }

Congratulations, you now have a self-updating windows service that once installed, can be managed remotely with little or no intervention.


History 

22/12/2013 - version 1 published



License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

AJSON
Chief Technology Officer
United Kingdom United Kingdom
Part-time software engineer, part-time student, always learning and refactoring my wetware. Happiest tucking into a big bowl of c# or python sprinkled with a crisp topping of JQuery...started with a single ZX80 a lifetime ago, now happily explore, build and create on interweb scale...
 

6/Oct/14 - "Full Calendar – A Complete Web Diary System for jQuery and C# MVC" - voted article of the day on ASP.net

Comments and Discussions

 
QuestionDo you have the updated production version PinmemberSilasPeter27-Aug-14 5:33 
QuestionLibraries in the same directory Pinmemberpepperz12-Mar-14 10:57 
AnswerRe: Libraries in the same directory PinmemberAJSON12-Mar-14 11:11 
GeneralRe: Libraries in the same directory Pinmemberpepperz17-Mar-14 14:09 
AnswerRe: Libraries in the same directory PinmemberAJSON17-Mar-14 23:20 
GeneralRe: Libraries in the same directory Pinmemberpepperz17-Mar-14 14:10 
QuestionDoes not work Pinmemberzimareff26-Feb-14 5:22 
Answer@zimareff PinmemberAJSON26-Feb-14 4:18 
GeneralRe: @zimareff Pinmemberzimareff26-Feb-14 5:25 
GeneralThanks Pinmemberparagpatel316-Jan-14 22:12 
AnswerRe: Thanks PinmemberAJSON6-Jan-14 22:21 
QuestionPull plugins from a database PinmemberKees van Spelde31-Dec-13 5:52 
AnswerRe: Pull plugins from a database PinmemberAJSON31-Dec-13 6:01 
GeneralRe: Pull plugins from a database PinmemberKees van Spelde31-Dec-13 6:10 
AnswerRe: Pull plugins from a database PinmemberAJSON31-Dec-13 6:18 
QuestionDrop the MSI PinmemberAndy Neillans30-Dec-13 21:45 
AnswerRe: Drop the MSI PinmemberAJSON30-Dec-13 22:43 
GeneralMy vote of 5 PinmemberHoangitk30-Dec-13 15:54 
AnswerRe: My vote of 5 PinmemberAJSON31-Dec-13 8:05 
QuestionExcellent! PinmemberDrew Stoddard30-Dec-13 10:14 
AnswerRe: Excellent! PinmemberAJSON30-Dec-13 10:35 
QuestionNice! PinmemberVolynsky Alex24-Dec-13 9:03 
AnswerRe: Nice! PinmemberAJSON24-Dec-13 9:32 
GeneralRe: Nice! PinmemberVolynsky Alex24-Dec-13 10:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141223.1 | Last Updated 31 Dec 2013
Article Copyright 2013 by AJSON
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid