Building smart and self-configurable Windows Services

Hi Community,

This post is about a question that many of us as architects and developers tend to ask ourselves, how can we build reliable yet flexible software that has the ability to adapt itself on the fly based on configuration changes? There are a few approaches to this, but Today I’d like to share a recent implementation I had to architect and pretty much build for one of my clients. My customer’s requirements comprised a web front-end built on ASP.NET MVC in conjunction with JQuery and JavaScript, since my customer did not have any Web Services and due to the delivery nature of this solution as an MVP, the orchestration and integration with backend (Database and Tableau) was responsibility of a Windows Service (Daemon) that can operate in two modes: 1) File operations manager and 2) “message” broker. At the same time, the daemon must be able to identify changes made to its configuration, stop itself, apply changes and restart itself all of this without any user interaction, this sounds cool, right?

In order to understand, how we can accomplish this it’s important to describe the foundational aspects of it or what is going to drive this behaviour. In this case, the daemon’s configuration.

<?xml version="1.0" encoding="utf-8" ?>

<ExecutionDaemon FileDelimiter="~" xmlns="http://schemas.bonafideideas.com/2016/ExecutionDaemon">

    <BrokerRole LocalFolderPath="" />

    <FileManagerRole LocalFolderPath="" RemoteFolderPath="" FileExtensionToBeProcessed="*.csv;*.xml"/>

    <ExternalToolPostExecution ImagePath="" Arguments="" DeleteFileOnCompletion="true" />

</ExecutionDaemon>

To support this functionality it was required to create a few classes that inherit from CustomConfiguration and ConfigurationElement. Config files can be written to/read from at runtime, but I needed to have something in place to handled this event and so there’s FileSystemWatcher. The code responsible for initializing the daemon, reload and apply changes and reconfiguration are listed below

/// <summary>

/// Initializes a new instance of the <see cref="ExecutionDaemon" /> class.

/// </summary>

/// <param name="args">The arguments.</param>

public ExecutionDaemon(string[] args) {

    _startArguments = args;

    ServiceName = Constants.ServiceName;

    _components = new System.ComponentModel.Container();

    Bootstrapper.Run(InitializeTypeContainer); // Take care of IoC plumbing and similar

    _configurationReader = TypeContainer.Resolve<IConfigurationReader>();

    var svcHomeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

    _configFileMonitor = new FileSystemWatcher(svcHomeDir) {

        EnableRaisingEvents = true, Filter = Constants.ConfigFileExtension,

        NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size

    };

    _configFileMonitor.Changed += (s, e) => ReloadAndApplyConfigChanges();

    Configure();

}


/// <summary>

/// Reloads the and apply configuration changes.

/// </summary>

private void ReloadAndApplyConfigChanges() {

    _logger.Log(Constants.PreReconfigureMessage);

    _configurationReader.Refresh();

    OnStop();

    Configure();

    OnStart(_startArguments);

    _logger.Log(Constants.PostReconfigureMessage);

}


/// <summary>

/// Configures this instance.

/// </summary>

private void Configure() {

    try {

        _mainWatcher?.Dispose();

        _isFileManagerMode = _startArguments?.Contains(Constants.FileManagerMode);

        var config = (CustomConfigSectionReader)_configurationReader.Configuration;

        _mainWatcher = new FileSystemWatcher(_isFileManagerMode.HasValue && _isFileManagerMode.Value ?

                                             config.FileManagerRole.LocalFolderPath :

                                             config.BrokerRole.LocalFolderPath) {

            EnableRaisingEvents = true,

            NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size

        };


        _mainWatcher.Changed += (s, e) => ProcessFileRequest(s, e);

    } catch (Exception ex) {

        _logger.Log(ex);

        _logger.Log(Constants.ConfigurationErrorStopServiceMessage);

        OnStop();

    }

}

it’s important to note the fact that I use NotifyFilter property, reason being is that FileSystemWatcher might “misfire” the Changed event, and in order to prevent this from happening we instruct the FileSystemWatcher to fire events only when (NotifyFilters.FileName | NotifyFilters.Size) have changed. If we make changes to the config file while the service is running, Eventvwr will inform of such changes (assuming Eventvwr had been cleared in advanced)

 

image

image

image image

 

As mentioned before, this Windows Service or Daemon has got dual execution mode, which means same executable but depending on the arguments it behaves and operates differently. The values entered in service property page (Start parameters) are not persisted, so I’ve got no idea why having that in the UI if it doesn’t do anything

 

image

 

but anyways we can set this parameters and make them “persistable” if we add them to the Registry in the following key – HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ExecutionDaemon

 

image

 

and voila! We have our dual operation Windows service deployed on two different servers and customers don’t have to deploy or start/stop anything but only make changes in the configuration file.

 

Regards,

 

Angel

Leave a Reply

Your email address will not be published. Required fields are marked *