Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Windows Service Auto-Update Plugin Framework

4.99/5 (27 votes)
31 Dec 2013CPOL6 min read 91.1K   2.9K  
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!

Image 1

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

Image 2

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.

Image 3

~ 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.

Image 4

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:

C#
// 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 version 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,
                                     // i.e.,: 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...)
}

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.

C#
// 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.

C#
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;
    ...

Loading into a domain:

C#
        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.

C#
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.

C#
        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!) ....

C#
// 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.

C#
public override object InitializeLifetimeService()
{
    return null;
}

Service Program

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

C#
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.

C#
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)