29 December 2010

Advanced C# Windows Service Installation

Click this link, to download source code.
Today, I'm going to show you how to efficiently create Windows Services and deployment projects, using C# in Visual Studio 2010.  These techniques illustrate methods of centralizing key information about the services included in the project, to prevent typos and other mistakes attributed to setting similar properties in decentralized locations.

This example is especially useful for projects that contain several services that use common code bases.  For example, I have a service suite that monitors several FTP drop directories on a server.  Each watcher performs different, custom actions; but, 90-95% of the code is common to all services.  For this and other reasons not discussed here, it makes sense to simply package all services in one project.  The more popular alternative is to create a common library, install to GAC, and create individual service solutions.

The bottom line is that I wanted to simplify management and deployment.  To simplify service registration, I created a "ServiceInfo" class attribute into which all service information is entered, instead of mucking about in several ProjectInstaller files.  View the full article, to get source code and a complete walk-through.

UPDATE 13 MAR 2012: I updated the code sections, to use formatted HTML, instead of using JavaScript code coloring.  The code was formatted a bit, for fit, and to make the LINQ more efficient.  Therefore, the code in the download won't be exactly the same, but it works.

My last post, How To Create a C# Windows Service Application discussed the basics of:

  1. What is a Windows Service?
  2. Why use a service?
  3. How to create a service
  4. How to create service installers
  5. How to create a Setup Project
If you are a savvy programmer, you probably noticed several inefficiencies that made the hairs on your neck stand on end.  I call this the Tin Foil effect -- it's the same reaction you get when chewing on a piece or tin foil (the real stuff, not aluminum foil).  Yuck!  Here, you will find a more streamlined version of the same demo project.

Why Centralized Code is Important

This is a no-brainer to most of you.  Simply put, if you have the same information that must be set to various objects in different places, throughout your code, it is easier and safer to store the values in a common place that is accessible to the various parts of code.  This saves you time, by providing a single place to change a value for the entire project or solution.  It is also extremely reliable, as you are no longer at risk of setting different values, when they should match.

Centralization is very much applicable to a Windows Service project.  For example, the name of a specific service is used in the service class, the ProjectInstaller object, and in the InstallerActions class.  Should you decide to change the name of a service in your project, you may forget to change it elsewhere, or mistype the name.


If the service name does not match that defined in the ProgramInstaller, the following error will appear, when attempting to manually start the service:
Error 1083: The executable program that this service is configures to run in does not implement the service.


Create a Windows Service Project

Create a project with the following:

  1. Create a new Windows Service Project in Visual Studio -- I left my project name as "WindowsService1"
  2. Create a class files:
    1. Attributes.cs
    2. Extensions.cs
    3. InstallActions.cs
    4. ProjectInstaller.cs
    5. DemoService2.cs
  3. Rename the Service1.cs file and class to "DemoService1".

Now, let's dump some code into these files.  Don't worry that InstallActions, ProjectInstaller, and Service2 are not components.  We don't need the extra baggage of the designer and resource files.  We won't need them, when we're through with this!

Program.cs

Add the Service2 class to the ServiceProcess array, even though we have not yet configured the Service2 class.  Your project will not compile until the Service2 class is coded.  We also need an enumeration of Event ID values.


using System.ServiceProcess;
namespace WindowsService1
{
    static class Program
    {
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] { 
        new DemoService1(),
        new DemoService2()
      };
            ServiceBase.Run(ServicesToRun);
        }
    }
 
    public enum GeneralEventIds
    {
        ServiceInfoMissing = 300100,
        ServiceInfoRetrievalFailed = 300101,
        ServiceConfigurationFailed = 300110
    }
}


The GeneralEventIds enumerations are used when writing to the system event log, in non-service classes.  These events report errors in the application, and are very helpful for debugging.

Attributes.cs

You probably guessed what this class will contain... an Attribute!  We are going to programmatically configure the services and installer by way of attributes.  An attribute contains meta data, and may be attached to a class.  We call this "decorating" the class with an attribute.


using System;
using System.ServiceProcess;
 
namespace WindowsService1
{
  [AttributeUsage(AttributeTargets.Class,
      AllowMultiple = false,
      Inherited = false)]
  public class ServiceInfo : Attribute
  {
    public ServiceInfo(
        string serviceName,
        string displayName,
        string description,
        System.ServiceProcess.ServiceStartMode startType,
        bool delayedAutoStart,
        bool autoLog,
        string eventLogName,
        bool canHandlePowerEvent,
        bool canPauseAndContinue,
        bool canShutdown,
        bool canStop,
        int exitCode)
    {
      if (String.IsNullOrEmpty(serviceName))
        throw new ArgumentException(
          "serviceName is null or empty.",
          "serviceName");
 
      if (String.IsNullOrEmpty(displayName))
        throw new ArgumentException(
          "displayName is null or empty.",
          "displayName");
 
      if (String.IsNullOrEmpty(description))
        throw new ArgumentException(
          "description is null or empty.",
          "description");
 
      if (String.IsNullOrEmpty(eventLogName))
        throw new ArgumentException(
          "eventLogName is null or empty.",
          "eventLogName");
 
      AutoLog             = autoLog;
      CanHandlePowerEvent = canHandlePowerEvent;
      CanPauseAndContinue = canPauseAndContinue;
      CanShutdown         = canShutdown;
      CanStop             = canStop;
      DelayedAutoStart    = delayedAutoStart;
      Description         = description;
      DisplayName         = serviceName;
      EventLogName        = eventLogName;
      ExitCode            = exitCode;
      ServiceName         = serviceName;
      StartType           = startType;
    }
 
    public bool AutoLog               { getprivate set; }
    public bool CanHandlePowerEvent   { getprivate set; }
    public bool CanPauseAndContinue   { getprivate set; }
    public bool CanShutdown           { getprivate set; }
    public bool CanStop               { getprivate set; }
    public bool DelayedAutoStart      { getprivate set; }
    public string Description         { getprivate set; }
    public string DisplayName         { getprivate set; }
    public int ExitCode               { getprivate set; }
    public string EventLogName        { getprivate set; }
    public string ServiceName         { getprivate set; }
    public ServiceStartMode StartType { getprivate set; }
  }
}


Notice this attribute may only be applied to a Class object -- services are class objects.  Otherwise, this looks like your typical property-laden attribute.  The properties stored in this attribute include everything needed to configure the service, ServiceInstaller object, and the InstallActions class.

Extensions.cs

Add a class file to the project, named "Extensions.cs".  The classes in this file will extend the System.ServiceProcess.ServiceBase class.


using System;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
 
namespace WindowsService1
{
    public static class Extensions
    {
        public static void ApplyServiceInfoAttributeValues(
            this ServiceBase service)
        {
            if (service == null)
                throw new ArgumentNullException("service",
                    "service is null.");
 
            ServiceInfo info;
            EventLog tempLog = new EventLog("Application"".",
                service.ServiceName);
 
            try
            {
                info = (service.GetType().GetCustomAttributes(
                  typeof(ServiceInfo), false).First()) as ServiceInfo;
 
                if (info == null)
                {
                    tempLog.WriteEntry(string.Format(
                        "The {0} service could not get the " +
                          "ServiceInfo object.",
                        service.ServiceName),
                      EventLogEntryType.Warning,
                      (int)GeneralEventIds.ServiceInfoRetrievalFailed);
                    return;
                }
            }
            catch (Exception ex)
            {
                tempLog.WriteEntry(string.Format(
                    "The {0} service could not get the ServiceInfo " +
                    "object, and threw an exception.\n\n{1}",
                    service.ServiceName,
                    ex.Message),
                  EventLogEntryType.Warning,
                  (int)GeneralEventIds.ServiceInfoMissing);
                return;
            }
 
            try
            {
                service.AutoLog = info.AutoLog;
                service.CanHandlePowerEvent = info.CanHandlePowerEvent;
                service.CanPauseAndContinue = info.CanPauseAndContinue;
                service.CanShutdown = info.CanShutdown;
                service.CanStop = info.CanStop;
                service.ServiceName = info.ServiceName;
                // EventLog.Source should always match the service name.
                service.EventLog.Source = info.ServiceName;
                service.EventLog.Log = info.EventLogName;
            }
            catch (Exception ex)
            {
                tempLog.WriteEntry(string.Format(
                    "The {0} service attempted to configure itself " +
                    "using bad data included in the ServiceInfo " +
                    "attribute, and threw an exception.\n\n{1}",
                    service.ServiceName,
                    ex.Message),
                  EventLogEntryType.Warning,
                  (int)GeneralEventIds.ServiceConfigurationFailed);
            }
        }
 
        public static void WriteExceptionToEventLog(
            this ServiceBase service,
            string customMessage,
            Exception exception,
            int eventId)
        {
            if (service == null)
                throw new ArgumentNullException("service",
                    "service is null.");
            if (exception == null)
                throw new ArgumentNullException("exception",
                    "exception is null.");
 
            if (service.EventLog == null)
                throw exception;
 
            string seperator = new String('-', 50);
            StringBuilder builder = new StringBuilder(customMessage);
 
            Exception exc = exception;
            do
            {
                builder.AppendLine();
                builder.AppendLine(seperator);
                builder.AppendLine(exc.Message);
                builder.AppendLine();
                builder.AppendLine("STACK TRACE:");
                builder.AppendLine(exception.StackTrace);
                exc = exc.InnerException;
            } while (exc != null);
 
            service.EventLog.WriteEntry(builder.ToString(),
              EventLogEntryType.Error,
              eventId);
        }
    }
}


ApplyServiceInfoAttributeValues
The ApplyServiceInfoAttributeValues method configures the service class with the values located in its ServiceInfo attribute.  This method allows the service class to be dynamically configured, upon instantiation.  The values are centrally managed in the ServiceInfo attribute, and will automatically get updated elsewhere, when changed.

Notice this method has its own EventLog object and a couple of Try/Catch blocks that may initially seem unnecessary.  The service's EventLog object may not be properly configured and ready for use, inside this extension method.  You can try using it instead of tempLog, but you'll either get an exception error or no log entry.

Try/Catch blocks are present, to report problems in the system event log, to help isolate problems.  Besides, we don't want the service to crash, due to an unhandled exception.

WriteExceptionToEventLog
The WriteExceptionToEventLog method does exactly as it describes -- this method writes details of a System.Exception object to the system log.  The method will write a custom message, which should provide contextual information about what was happening when the exception was thrown.  The Message and CallStack properties of the Exception object are appended to the message, and the same for ever inner exception is also written.  This provides some very useful debugging information.

DemoService1.cs

Let's get Service1 set up.  This service will monitor file system directory C:\Temp\Service1.  Any changes made to the contents of the directory will be reported in the system event log.


using System;
using System.IO;
using System.ServiceProcess;
 
namespace WindowsService1
{
  [ServiceInfo("DemoService1""Demo Service 1",
      "This is a demonstration service.",
      ServiceStartMode.Automatic, truetrue"Application",
      falsefalsefalsetrue, 0)]
  public partial class DemoService1 : ServiceBase
  {
    FileSystemWatcher _watcher;
 
    // Event ID values may be anything you want.
    enum EventIds
    {
      WatcherEvent_Changed                 = 10,
      WatcherEvent_Created                 = 11,
      WatcherEvent_Deleted                 = 12,
      WatcherEvent_Renamed                 = 13,
      WatcherEvent_Error                   = 19,
      Initialization_GeneralFailure        = 100,
      Initialization_WatcherDir_NotExist   = 101,
      Initialization_WatcherDir_NoAccess   = 102,
      Initialization_WatcherDir_OtherError = 103,
      Start_GeneralFailure                 = 200,
      Start_InitializationFailure          = 201,
      Stop_GeneralFailure                  = 300
    }
 
    public DemoService1()
      : base()
    {
      //InitializeComponent();
      this.ApplyServiceInfoAttributeValues();
 
      // The directory path should be stored in the application settings!
      string path = @"C:\Temp\DemoService1";
      if (!Directory.Exists(path))
      {
        try
        {
          Directory.CreateDirectory(path);
        }
        catch (Exception exc)
        {
          this.WriteExceptionToEventLog(
            "Could not create directory for the FileSystemWatcher: " + path,
            exc,
            (int)EventIds.Initialization_WatcherDir_NotExist);
        }
      }
 
      _watcher = new FileSystemWatcher(path);
      _watcher.Changed += new FileSystemEventHandler(watcher_Changed);
      _watcher.Created += new FileSystemEventHandler(watcher_Created);
      _watcher.Deleted += new FileSystemEventHandler(watcher_Deleted);
      _watcher.Error += new ErrorEventHandler(watcher_Error);
      _watcher.Renamed += new RenamedEventHandler(watcher_Renamed);
    }
 
    protected override void OnStart(string[] args)
    {
      _watcher.EnableRaisingEvents = true;
    }
 
    protected override void OnStop()
    {
      _watcher.EnableRaisingEvents = false;
    }
 
    void watcher_Changed(object sender, FileSystemEventArgs e)
    {
      this.EventLog.WriteEntry(
          "The following file system object was changed:\n" + e.FullPath,
          System.Diagnostics.EventLogEntryType.Information,
          (int)EventIds.WatcherEvent_Changed);
    }
 
    void watcher_Created(object sender, FileSystemEventArgs e)
    {
      this.EventLog.WriteEntry(
        "The following file system object was created:\n" + e.FullPath,
        System.Diagnostics.EventLogEntryType.Information,
        (int)EventIds.WatcherEvent_Created);
    }
 
    void watcher_Deleted(object sender, FileSystemEventArgs e)
    {
      this.EventLog.WriteEntry(
        "The following file system object was deleted:\n" + e.FullPath,
        System.Diagnostics.EventLogEntryType.Information,
        (int)EventIds.WatcherEvent_Deleted);
    }
 
    void watcher_Error(object sender, ErrorEventArgs e)
    {
      this.WriteExceptionToEventLog(
        "The FileSystemWatcher object reported an error.",
        e.GetException(),
        (int)EventIds.WatcherEvent_Changed);
    }
 
    void watcher_Renamed(object sender, RenamedEventArgs e)
    {
      this.EventLog.WriteEntry(
        "The following file system object was renamed:\n" + e.FullPath,
        System.Diagnostics.EventLogEntryType.Information,
        (int)EventIds.WatcherEvent_Renamed);
    }
  }
}


Unlike in my previous blog post, we are not creating an EventLog object in this class.  This class inherits an EventLog object from System.ServiceProcess.ServiceBase.  We should use this log instead of creating one, to save resources and to reduce code.  The previous post included a separate EventLog object, to avoid confusing less-experienced developers.
EventIds
Each service has a unique set of event IDs reported in the system event log.  Because enumerations are assigned an integer value, we can use them to centralize management of the event IDs.  The enumeration names also provide a descriptive representation of the value, when writing code, too.

ServiceInfo
The ServiceInfo attribute does not include the directory path to watch; because, that information is not pertinent to the configuration of a general services.

DemoService1
Because this default class was created as a component, it contains extra files (DemoService1.Designer.cs and DemoService1.resx) to support the component design interface.  Therefore, InitializeComponent() is called in the constructor.  Just leave it alone.  The Service2.cs file will not need to call the method, because we didn't create it as a component, and don't need the design interface, following the methods described in this post.

The ApplyServiceInfoAttributeValues extension is called, to set properties inherited from System.ServiceProcess.ServiceBase with the values from the ServiceInfo attribute.  This can only be done in the class constructor.

Finally, the FileSystemWatcher object is configured.  If the watched directory does not exist, we attempt to create the directory.


DemoService2.cs

DemoService2.cs is very easy to set up.  Simply copy everything from DemoService1.cs, and then paste it into DemoService2.cs, replacing all preexisting code.  You must change the following things:

  • Change the ServiceInfo attribute's serviceName input parameter value to "DemoService2"
  • Change the ServiceInfo attribute's displayName input parameter value to "DemoService2"
  • Change the last directory in the path of watchedDirectory to "DemoService2".
  • Rename the class to "DemoService2"
  • Rename the constructor to "DemoService2"
  • Delete the call to InitializeComponent from the class constructor -- the method does not exist!

That wasn't so bad, and we saved a lot of vertical space on this page, too.


ProjectInstaller.cs

This is where we start realizing the value of using the ServiceInfo attribute.  Many of the ServiceBase properties must match those of the ServiceInstaller objects included in this class.  By centralizing these values in the attribute, we eliminate human error (typos), and make the values highly manageable.


using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.ServiceProcess;
 
namespace WindowsService1
{
  [RunInstaller(true)]
  public partial class ProjectInstaller
    : System.Configuration.Install.Installer
  {
    public ProjectInstaller()
    {
      // Ensure the Installers collection is clear!
      this.Installers.Clear();
 
      ServiceProcessInstaller processInstaller = new ServiceProcessInstaller();
      processInstaller = new System.ServiceProcess.ServiceProcessInstaller();
      processInstaller.Account = ServiceAccount.LocalSystem;
 
      Assembly assembly = Assembly.GetExecutingAssembly();
      ServiceInfo info;
      ServiceInstaller serviceInstaller;
      Type serviceInfoType = typeof(ServiceInfo);
 
      var types = assembly.GetTypes().Where(t =>
        t.BaseType == typeof(System.ServiceProcess.ServiceBase));
      if (types.Any())
      {
        foreach (Type t in types)
        {
          try
          {
            info = (t.GetCustomAttributes(serviceInfoType,
              false).First()) as ServiceInfo;
          }
          catch
          { // Ignore the type.
            continue;
          }
 
          if (info != null)
          {
            serviceInstaller                  = new ServiceInstaller();
            serviceInstaller.DelayedAutoStart = info.DelayedAutoStart;
            serviceInstaller.Description      = info.Description;
            serviceInstaller.DisplayName      = info.DisplayName;
            serviceInstaller.ServiceName      = info.ServiceName;
            serviceInstaller.StartType        = info.StartType;
            processInstaller.Installers.Add(serviceInstaller);
          }
        }
      }
      this.Installers.Add(processInstaller);
    }
  }
}


This class is decorated with the RunInstaller attribute, which indicates the class contains installation event handlers to be called during the installation process.  The ServiceInstaller objects install the individual services to the host machine, including registration of the assembly as a service.  Each of these objects in added to a collection in the ProjectInstaller object, which installs the project as a whole.  This configures items for all child installation objects, such as the account that will execute the services.
Using reflection, we programmatically identify service classes decorated with the ServiceInfo attribute.  Then, we create a ServiceInstaller object for each.  The ServiceInstaller properties are set to values from the ServiceInfo attribute.

Note the use of the Any LINQ method.  This is a far more efficient method of determining if an IEnumerable collection contains items.  Testing the Count or Length property for a zero (0) value requires traversal of the collection, which wastes time.  The Any method simply returns a boolean value, indicating the collection contains at least one item.  This is very fast, by virtue of simply testing only the first collection pointer for a non-zero value.  Aw, snap!  That's fast!


InstallActions.cs

This class also contains installation event handlers.  However, these pertain to the setup process as a whole, instead of installing portions of the project.  When installing or removing services, they must be stopped, before attempting to replace or remove their execution assembly.  This is the place to do just that.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceProcess;
using WindowsService1;
 
[RunInstaller(true)]
public class InstallActions : Installer
{
  ServiceController _controller;
  List<ServiceInfo> infoList;
 
  public InstallActions()
    : base()
  {
    this.AfterInstall += new InstallEventHandler(InstallActions_AfterInstall);
    this.BeforeInstall += new InstallEventHandler(InstallActions_BeforeInstall);
    this.BeforeUninstall += new InstallEventHandler(InstallActions_BeforeUninstall);
 
    infoList = new List<ServiceInfo>();
    object[] custAttribs;
    Assembly.GetAssembly(typeof(ServiceInfo)).GetTypes()
    .Where(t => t.BaseType == typeof(
    System.ServiceProcess.ServiceBase)).ToList()
    .ForEach(t =>
    {
      try
      {
        custAttribs = t.GetCustomAttributes(
        typeof(ServiceInfo), false);
        if (custAttribs.Any())
        {
          for (int i = 0, j = custAttribs.Length; i < j; i++)
          {
            if (custAttribs[i] is ServiceInfo)
              infoList.Add(custAttribs[i] as ServiceInfo);
          }
        }
      }
      finally { }
    });
  }
 
  void InstallActions_AfterInstall(object sender,
  InstallEventArgs e)
  {
    // Start the services.
    SetServiceStates(true);
  }
 
  void InstallActions_BeforeInstall(object sender,
  InstallEventArgs e)
  {
    // Stop the services and register event log sources.
    SetServiceStates(false);
    foreach (ServiceInfo info in infoList)
    {
      // The EventLog.Source value should match service name.
      if (!EventLog.SourceExists(info.ServiceName))
      {
        EventLogInstaller eventLogInstaller =
        new EventLogInstaller();
        eventLogInstaller.Source = info.ServiceName;
        Installers.Add(eventLogInstaller);
      }
    }
  }
 
  void InstallActions_BeforeUninstall(object sender,
  InstallEventArgs e)
  {
    // Stop the services.
    SetServiceStates(false);
  }
 
  void SetServiceStates(bool startService)
  {
    ServiceController[] services =
    ServiceController.GetServices();
    foreach (ServiceInfo info in infoList)
    {
      _controller =
      new ServiceController(info.ServiceName, ".");
      if (services.Contains(_controller))
      {
        if (startService)
        {
          if (_controller.Status != ServiceControllerStatus.Running
          && _controller.Status != ServiceControllerStatus.StartPending)
            _controller.Start();
        }
        else if (_controller.Status !=
        ServiceControllerStatus.Stopped)
        {
          if (_controller.Status !=
          ServiceControllerStatus.StopPending)
          {
            _controller.Stop();
          }
 
          _controller.WaitForStatus(
          ServiceControllerStatus.Stopped,
          new TimeSpan(0, 0, 30));
        }
      }
    }
  }
}


InstallActionsLike the ProjectInstaller constructor, this class constructor uses reflection, to build a List of ServiceInfo attributes.  These are used to create system event log sources and to start and stop services by name.

InstallActions_AfterInstall
This event handler calls SetServiceStates, to start the services included in this project.  Services are allowed 30 seconds to start, so don't overload the service constructor and/or OnStart handler thread.

InstallActions_BeforeInstall and InstallActions_BeforeUninstall
Both of these event handlers call SetServiceStates, to stop all services that may be replaced by this install process.  Services are allowed 30 seconds to stop, so don't overload the OnStop handler thread.

SetServiceStates
This method either starts or stops all services in this project, installed to the host machine.  The services must be decorated with the ServiceInfo attribute, to be affected by this method.


Create a Setup Project

The last part of creating this solution is to add a setup project.  Simply follow the process noted in my previous blog post,  How To Create a C# Windows Service Application.  The important part is to configure the Install and Uninstall custom actions.


Test the Services

Build the solution.  This will not cause the Setup1 project to compile WindowsService1 into a setup executable.  Right-click the Setup1 project in the Solution Explorer window, and then select Rebuild in the context menu.  You'll notice compilation takes quite a bit longer, this time.

I prefer to use a virtual machine, when testing applications that alter the system registry, install services, and/or event log sources.  Whatever virtual machine client you use doesn't matter -- just ensure you configure it to allow rolling back changes to the virtual hard drive.

  1. Copy the Setup.exe and Setup.msi files to the virtual machine, and then execute them.  If you are testing directly on your workstation (scary!), simply right-click the Setup1 project in the Solution Explorer window, and then select Install.
  2. Proceed through the installation wizard
  3. Open the host system's Services window -- you'll see DemoService1 and DemoService2 are present -- notice they show the information set in the ServiceInfo attributes

  4. Start the services, if they are not already started
  5. Open the host system's Event Viewer utility
  6. View the Application Events logs -- you'll see an entry from MsiInstaller, indicating the installation was successful, and a couple of entries from our services, indicating they have started
  7. Create or copy any type of file to the C:\Temp\DemoService1 directory
  8. Create or copy any type of file to the C:\Temp\DemoService2 directory
  9. Refresh the Application Events view in the Event Viewer window -- each of our services have logged an entry, indicating a file was created in their respective directories

4 comments:

  1. HI,

    I've just downloaded your sample project, I've installed it without any problems. The problem I am encountering on my project is updating the windows service.

    When I increment the build number and run the installer I get "specified service already exists" do you know of a away to get around this.

    I have my service installed, just having troubles updating it. I don't want the user to have to reboot either. Any help would be great.

    Thanks

    ReplyDelete
    Replies
    1. Hello, Ashley.

      Yes, I know exactly what you're talking about. Instead of un-installing the service after making code changes:

      1. STOP the service

      2. Copy newly-compiled files into the service installation directory

      3. START the service

      Upon starting the service, it will use the new files. You can automate this process, using a PowerShell or batch script.

      Delete
  2. NOTE: I believe there is a bug in the code on this post, but I can't remember if I corrected it. I'll check it out.

    ReplyDelete

Please provide details, when posting technical comments. If you find an error in sample code or have found bad information/misinformation in a post, please e-mail me details, so I can make corrections as quickly as possible.