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
-
- public interface IPlugin
- {
- string PluginID();
- bool TerminateRequestReceived();
- string GetName();
- string GetVersion();
- bool Start();
- bool Stop();
- void LogError(string Message, EventLogEntryType LogType);
- string RunProcess();
- void Call_Die();
- void ProcessEnded();
-
-
- event EventHandler<plugineventargs> CallbackEvent;
- PluginStatus GetStatus();
- }
-
- </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.
-
-
- [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)
- {
-
- this.MessageType = messageType;
- this.ResultMessage = resultMessage;
- this.ResultValue = resultValue;
- this.EventAction = eventAction;
- this.executingDomain = AppDomain.CurrentDomain.FriendlyName;
- this.pluginName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
-
- }
- }
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.
- 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.
- 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)
- {
-
-
- if (e.MessageType == PluginEventMessageType.Action)
- {
- ....
- else if (e.EventAction.ActionToTake == PluginActionType.Unload)
- {
- ....
- else
- {
- if (Callback != null)
- {
- 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 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.
- public bool Stop()
- {
- if (_Status == PluginStatus.Running)
- {
- _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;
- }
-
- ...
-
-
- 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;
- }
- }
The "command / control" plugin simulates requesting the service update itself (hey, finally, the reason we came to this party!) ....
-
- 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));
- }
-
-
- DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND ***"));
- PluginEventAction actionCommand = new PluginEventAction();
- actionCommand.ActionToTake = PluginActionType.TerminateAndUnloadPlugins;
- DoCallback(new PluginEventArgs(PluginEventMessageType.Action, null, actionCommand));
- DoCallback(new PluginEventArgs(PluginEventMessageType.Message, "*** Sending UPDATE SERVICE WITH INSTALLER COMMAND - COMPLETE ***"));
- Call_Die();
-
- }
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);
- }
- 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);
-
- if (UseInstallerVersion == 1)
- {
- EventLogger.LogEvent("Using installer 1", EventLogEntryType.Information);
- UseInstallerVersion = 2;
-
- }
- else if (UseInstallerVersion == 2)
- {
- EventLogger.LogEvent("Using installer 2", EventLogEntryType.Information);
-
- UseInstallerVersion = 1;
- }
- }
- }
- }
Congratulations, you now have a self-updating Windows service that once installed, can be managed remotely with little or no intervention. Happy Service Coding! :)