Creating an Auto-Updating C# Windows Service

Don’t you love it when problems that seemed insurmountable in the past turn out to be fairly trivial to implement because of new techniques/libraries? I just had one of those moments and thought it would be instructive to share.

The Need:

Back in 2008, I was working for Dell as a software architect, and we had several Windows Services that were core to the product I was working on. They existed on many different servers, and updating them was a time consuming process. I had worked with auto-updating client applications, but I hadn’t seen any patterns to handle automatically updating a service. When you have questions about programming, StackOverflow is the place to go, so I asked:

There was some guidance, but no easy answer. The consensus was that this was a difficult problem. I got busy with other things and tabled the effort. Then, I got a note from a buddy from the local user group asking how I solved the issue. He was facing a similar problem with a watcher service he was deploying to customers’ servers, so the option of manual updates was out.

The Solution:

Here is a quick POC I threw together to illustrate the use of MEF, a FileSystemWatcher, a BackgroundWorker, and an AppDomain to allow a running application to be updated.

First, I am not building actual Windows Services since the housekeeping involved in it would obscure the point of this post. I am using a simple Windows Console application with a While (True) loop. Second, I am not doing any real work with the example, again to avoid obscuring the technique I am trying to illustrate.

That said, the associated solution has five projects:

  • POC10 – The surrogate for the service
  • Interfaces – A shared project containing the interface to the worker
  • Foreman – The program that actually uses the workers to accomplish some task
  • DoWorkOne – Represents the deployed functionality
  • DoWorkTwo – The updated functionality we want to add to the running application

Starting at the Interface, it is what we use to expose the functions that do the actual work. We use it to de-couple the work logic from the control logic. Here it is:

    public interface IDoWork

    {

        void DoThis(string one);

        void DoThat(string one);

    }

The DoWorkOne and DoWorkTwo are single-class assemblies that implement and Export the IDoWork interface:

namespace DoWorkOne

{

    [Export(typeof(IDoWork))]

    public class Class1 : IDoWork

    {

        public void DoThis(string one)

        {

            Console.WriteLine(string.Format("1: Do This {0}", one));

        }

 

        public void DoThat(string one)

        {

            Console.WriteLine(string.Format("1: Do That {0}", one));

        }

    }

}

namespace DoWorkTwo

{

    [Export(typeof(IDoWork))]

    public class Class1 : IDoWork

    {

        public void DoThis(string one)

        {

            Console.WriteLine(string.Format("2: Do This {0}", one));

        }

 

        public void DoThat(string one)

        {

            Console.WriteLine(string.Format("2: Do That {0}", one));

        }

    }

}


The Foreman is a console application that uses MEF to load the worker and then uses it to print out a couple of lines of text to the console. It has a local Worker class that imports a class implementing the IDoWork interface:

    public class Worker

    {

        [Import(typeof(IDoWork))]

        public IDoWork CurrentWorker { get; set; }

    }

The main program gets a new worker from a known assembly and uses it:

    /// <summary>

    /// This is the Foreman class that acts as a proxy for the individual worker DLLs.

    /// </summary>

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("starting");

            IDoWork worker = GetWorker();

            worker.DoThis("hello world");

            worker.DoThat("Hi again");

            Console.WriteLine("finished");

        }

        static IDoWork GetWorker()

        {

            Assembly asm = Assembly.LoadFrom("DoWork.dll");

 

            var catalog = new AssemblyCatalog(asm);

 

 

            var worker = new Worker();

            var container = new CompositionContainer(catalog);

 

            container.ComposeParts(worker);

 

            if (worker.CurrentWorker != null)

            {

                return worker.CurrentWorker;

            }

            return null;

        }

    }

The beauty of using this pattern is that you can execute the Foreman.exe from the command line to allow testing of the functionality outside of the service.

The magic happens in the POC10 program. As mentioned above, it is just a console application for this example. Here is the full code:

namespace POC10

{

    class Program

    {

        static AppDomain _domain;

        static BackgroundWorker _backgroundWorker;

        static void Main(string[] args)

        {

            if (!Directory.Exists(Environment.CurrentDirectory + "\\updates"))

                Directory.CreateDirectory(Environment.CurrentDirectory + "\\updates");

            FileSystemWatcher watcher = new FileSystemWatcher(Environment.CurrentDirectory + "\\updates", "*.dll");

 

            //Copy the inital worker into the correct location

            InitializeWorker();

 

            //Build a worker thread to host the Foreman task in the alternate domain

            _backgroundWorker = new BackgroundWorker();

            _backgroundWorker.WorkerSupportsCancellation = true;

            _backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);

            _backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);

 

            while (true)

            {

                //start it working

                _backgroundWorker.RunWorkerAsync();

 

                watcher.WaitForChanged(WatcherChangeTypes.Created);

 

                ChangeWorker();

                //stop it

                _backgroundWorker.CancelAsync();

                //wait for the background thread to finish

                while (_backgroundWorker.IsBusy)

                {

                    Thread.Sleep(new TimeSpan(0, 0, 0, 0, 100));

                }

            }

        }

 

        static void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

        {

            // Unload the application domain.

            AppDomain.Unload(_domain);

        }

 

        static void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

        {

            //Set up your new app domain, setting ShadowCopyFiles to true so that you can replace

            // the worker while running

            AppDomainSetup domaininfo = new AppDomainSetup();

            domaininfo.ApplicationBase = Environment.CurrentDirectory;

            domaininfo.ShadowCopyFiles = "true";

 

            // Create the application domain.

            _domain = AppDomain.CreateDomain("MyDomain", null, domaininfo);

 

            while (true)

            {

                //Get your foreman running

                _domain.ExecuteAssembly("Foreman.exe");

                //take a short nap

                Thread.Sleep(new TimeSpan(0, 0, 1));

                //check to see if there is a pending cancelation of this thread, and stop if there is

                if (_backgroundWorker.CancellationPending)

                    break;

            }

        }

 

        private static void InitializeWorker()

        {

            File.Copy("workers\\DoWorkOne.dll", "DoWork.dll", true);

        }

 

        private static void ChangeWorker()

        {

            string newWorker = Directory.GetFiles(Environment.CurrentDirectory + "\\updates", "*.dll")[0];

            //wait until the file has fully been stored on the disk

            while (true)

            {

                try

                {

                    using (File.OpenWrite(newWorker)) { }

                }

                catch

                {

                    continue;

                }

                break;

            }

 

            //copy and clear the update

            File.Copy(newWorker, "DoWork.dll", true);

            File.Delete(newWorker);

        }

    }

}

Walking through the parts, it begins by setting up a FileSystemWatcher for a known location on the file system:

if (!Directory.Exists(Environment.CurrentDirectory + "\\updates")){

Directory.CreateDirectory(Environment.CurrentDirectory + "\\updates");

}

      

FileSystemWatcher watcher = new FileSystemWatcher(Environment.CurrentDirectory + "\\updates", "*.dll");

If the updates directory doesn’t exist, create it and then start watching it for .dll files.

Next, set up the initial worker by making a copy of the DoWorkOne.dll into the working directory:

//Copy the inital worker into the correct location

InitializeWorker();

The method called is just a file copy:

private static void InitializeWorker()

{

File.Copy("workers\\DoWorkOne.dll", "DoWork.dll", true);

}

Next, launch a BackgroundWorker thread to run Foreman.exe in a separate AppDomain within an infinite loop:

//Build a worker thread to host the Foreman task in the alternate domain

_backgroundWorker = new BackgroundWorker();

_backgroundWorker.WorkerSupportsCancellation = true;

_backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);

_backgroundWorker.RunWorkerCompleted += new

RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);

And the backgroundWorker_DoWork implementation is as follows:

        static void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

        {

            //Set up your new app domain, setting ShadowCopyFiles to true so that you can

            // replace the worker while running

            AppDomainSetup domaininfo = new AppDomainSetup();

            domaininfo.ApplicationBase = Environment.CurrentDirectory;

            domaininfo.ShadowCopyFiles = "true";

 

            // Create the application domain.

            _domain = AppDomain.CreateDomain("MyDomain", null, domaininfo);

 

            while (true)

            {

                //Get your foreman running

                _domain.ExecuteAssembly("Foreman.exe");

                //take a short nap

                Thread.Sleep(new TimeSpan(0, 0, 1));

                //check to see if there is a pending cancelation of this thread,

                // and stop if there is

                if (_backgroundWorker.CancellationPending)

                    break;

            }

        }

An important part of the AppDomain setup is to set the ShadowCopyFiles to "true" (yes, it is a string rather than a bool), which will allow you to modify the .exe and .dll files you are using within the domain. Another item to note is that you are using global static variables to hold references to the AppDomain and BackgroundWorker:

        static AppDomain _domain;

        static BackgroundWorker _backgroundWorker;

This is required to allow you to break out of the worker loop when you have asked the BackgroundWorker thread to cancel.

The last thing to do in the main is to start your infinite loop, start the background thread, and then wait for the change in the update directory:

            while (true)

            {

                //start it working

                _backgroundWorker.RunWorkerAsync();

 

                watcher.WaitForChanged(WatcherChangeTypes.Created);

 

                ChangeWorker();

                //stop it

                _backgroundWorker.CancelAsync();

                //wait for the background thread to finish

                while (_backgroundWorker.IsBusy)

                {

                    Thread.Sleep(new TimeSpan(0, 0, 0, 0, 100));

                }

            }

When you have a change, change out the worker dll, call for the background thread to exit, wait for it to exit, and then start over again with the updated program. Changing the worker also has a bit of magic, because the changed event is thrown when the file copy begins, not when it completes. This means you need to wait until the copy completes. One way to watch for this is to try and open the file for write, and keep trying until you can.

        private static void ChangeWorker()

        {

            string newWorker = Directory.GetFiles(Environment.CurrentDirectory + "\\updates", "*.dll")[0];

            //wait until the file has fully been stored on the disk

            while (true)

            {

                try

                {

                    using (File.OpenWrite(newWorker)) { }

                }

                catch

                {

                    Thread.Sleep(new TimeSpan(0, 0, 0, 0, 100));

                    continue;

                }

                break;

            }

 

            //copy and clear the update

            File.Copy(newWorker, "DoWork.dll", true);

            File.Delete(newWorker);

        }

What does it look like when running? Well, it looks like this:

C# example 1

To update the DLL used by the Foreman task, just copy your new one into the updates folder:

C# example 2
877-277-1044
Categories

Copyright 2013 Magenic, All rights Reserved