Sunday, April 10, 2011

Self Updating WCF Service

Recently I was in a meeting where my idea of a self-updating WCF service was laughed at. This post will contain a very rough proof of concept that the idea is possible. The solution is pretty simple.

1) Self hosted WCF service has a file watcher looking for a file to be created.
2) WCF service has an "update" method where it creates a file to trigger an update
3) Self hosted service reloads the new assembly by way of reflection.

I have skipped over a lot of the details in this proof of concept. Things to look at would be:

1) How does the update file get there (could be via WCF Service)
2) File watcher is not the only way to do this. Maybe nServiceBus? I would have to look into it more.
3) DLL Needs to be verified somehow before it is just loaded automatically. Could be done via signed dll's, or another method? Open to suggestions.

Anyways, here is the code.

WCF Service
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;

namespace WCFSelfUpdater
{
    public class Service1 : IService1
    {
        private string versionNumber = "1.1";

        /// 
        /// Version number in original program is 1.0.  Changed to 1.1
        /// If you want to reproduce this, you need to create 2 dll's each with a different 
        /// version number.
        /// 
        /// 
        public string GetVersionNumber()
        {
            return versionNumber;
        }

        /// 
        /// Method could write the stream out to the disk and then move it (atomic operation)
        /// to where it needs to be.  In this case I just skipped over all of the implementation'
        /// details to just show that the idea was possible.
        /// 
        ///         /// 
        public bool Update(Stream stream)
        {
            try
            {
                using (TextWriter writer = new StreamWriter(@"c:\temp\shamir.dll"))
                {
                    writer.WriteLine("PLEASE UPDATE");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("ERROR" + e.Message);
            }

            return false;

        }
    }
}


Self Hosting Command Line Application
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Reflection;
using System.IO;
using System.Threading;

namespace WCFSelfUpdaterHost
{
    class Program
    {
        static bool NeedsUpdate = false;

        static void Main(string[] args)
        {
            try
            {
                //Step 1, create a file watcher to check for "update.dll" to be placed
                FileSystemWatcher watcher = new FileSystemWatcher(@"c:\temp","shamir.dll");
                watcher.EnableRaisingEvents = true;
                watcher.Changed += new FileSystemEventHandler(watcher_Changed);
                Uri baseAddress = new Uri("http://localhost:9999/hello");
                
                // initial setup of WCF service with version 1.0
                Assembly assem = Assembly.LoadFile(@"D:\work\WCFSelfUpdater\WCFSelfUpdater\bin\WCFSelfUpdater.dll");
                Type serviceType = assem.GetType("WCFSelfUpdater.Service1");

                bool done = false;

                // CAUTION::: Done is never set to true.  I ran this in debug mode so didn't have a problem!!!!
                while (!done)
                {
                    var result = RunService(serviceType, baseAddress);
                    if (result == ResultCode.NeedsUpdate)
                    {
                        // Load new service
                        assem = Assembly.LoadFile(@"D:\work\WCFSelfUpdater\WCFSelfUpdater\bin\WCFSelfUpdater1.dll");
                        serviceType = assem.GetType("WCFSelfUpdater.Service1");

                        // This code simple checks to make sure the version changed.
                        var obj = Activator.CreateInstance(serviceType);
                        MethodInfo me = serviceType.GetMethod("GetVersionNumber");
                        var result1 = me.Invoke(obj, null);
                        Console.WriteLine("NEW VERSION NUMBER:" + result1);

                        // Need this to keep loop below going
                        NeedsUpdate = false;
                    }
                    else
                    {
                        done = true;
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error received.  Enter to exit." + e.Message, e);
                Console.ReadLine();
            }

            Console.WriteLine("Done execution: enter to exit");
            Console.ReadLine();
        }


        static void watcher_Changed(object sender, FileSystemEventArgs e)
        {
            NeedsUpdate = true;
        }


        static ResultCode RunService(Type type, Uri baseAddress)
        {
            using (ServiceHost host = new ServiceHost(type, baseAddress))
            {
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;
                smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
                host.Description.Behaviors.Add(smb);

                host.Open();

                Console.WriteLine("Enter to stop");
                while (!NeedsUpdate) { Thread.Sleep(100); }

                host.Close();
                return ResultCode.NeedsUpdate;
            }
        }
    }

    enum ResultCode
    {
        Done,
        NeedsUpdate
    }
}


Calling application
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

namespace WCFClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var client = new ServiceReference1.Service1Client();
            try
            {

                StreamReader reader = new StreamReader(@"c:\temp\WCFSelfUpdater.dll");
                

                client.Open();
                Console.WriteLine(client.GetVersionNumber());
                // doesn't matter what file is being read in as it isn't currently
                // used in the WCF Service
                Console.WriteLine(client.Update(reader.BaseStream));
                // Sleeping to allow time to reload the wcf service.
                Thread.Sleep(10000);
                Console.WriteLine(client.GetVersionNumber());
                Console.ReadLine();
            }
            catch (Exception e)
            {
                Console.WriteLine("Error received.  Enter to exit" + e.Message, e);
                Console.ReadLine();
            }
            finally
            {
                client.Close();
            }
        }
    }
}



In any event, this proof of concept is pretty rough. There is little in the way of security, etc implemented. It is good to know that the idea is possible, and only took about an hour to research and implement.

Resources used:
http://dranaxum.wordpress.com/2008/02/25/dynamic-load-net-dll-files-creating-a-plug-in-system-c/
http://www.c-sharpcorner.com/UploadFile/mokhtarb2005/FSWatcherMB12052005063103AM/FSWatcherMB.aspx
http://www.csharp-examples.net/reflection-examples/