Tuesday, July 3, 2012

A Simple Framework for Windows Services 1 of 3

Source code for Service Framework posts.
I’ve had to write several windows services in the past. I developed a simple framework to make it easier to create and test windows service projects. The framework builds up a couple layers, with the innermost layer independent of the outer layers. So you can choose how much of the framework you want to use.
The inner layer is composed of the ExecutableServiceBase class. It puts in place behavior that automatically detects if the program is a debug build, and is UserInteractive, and if it is, runs the program as an interactive console application. If not, it assumes it has been invoked by Window’s Service Manager, and lets the code run as a service. This makes it very convenient to test the service by running it from Visual Studio during development. When run as a console application, standard input is monitored for single letter commands to simulate Service Manager events such as Pause, Continue, Stop, etc.
When using ExecutableServiceBase, your Windows service’s Program class is simplified to:
using SoftWx.ServiceProcess;

namespace ServiceTest {
    class Program {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static void Main(string[] args) {
            ExecutableServiceBase.Run(args, new MyService());
        }
    }
}

The static ExecutableServiceBase.Run method expects an array of ExecutableServiceBase descendents, described a little further down. The Run method does the initial work of determining whether it’s being run as a service or a console app, and then gets the service started as needed for that environment.
// Copyright ©2012 SoftWx, Inc.
// Released under the MIT License the text of which appears at the end of this file.
// version 1.3 - 7/25/2012
// <authors> Steve Hatchett

using System;
using System.Diagnostics;
using System.ServiceProcess;

namespace SoftWx.ServiceProcess {
    /// <summary>
    /// Class for Windows Services, exposing methods that allow
    /// execution control outside of the normal ServiceController
    /// pattern. These methods can facilitate activities such as 
    /// running of services within a console app.
    /// </summary>
    public class ExecutableServiceBase : ServiceBase {
        private string[] programArgs;   // arguments that were passed into Program.Main
        private int maxWaitSecs = 120;  // maximum seconds to wait for Stop to complete

        /// <summary>
        /// Run the service. Depending on how it was started, it may
        /// run as a windows service, or, if the application is user
        /// interactive, and was compiled with DEBUG, then it's run
        /// as a console app.
        /// </summary>
        /// <param name="services">The services to be run.</param>
        public static void Run(params ExecutableServiceBase[] services) {
            Run(new string[0], services);
        }

        /// <summary>
        /// Run the service. Depending on how it was started, it may
        /// run as a windows service, or, if the application is user
        /// interactive, and was compiled with DEBUG, then it's run
        /// as a console app.
        /// </summary>
        /// <param name="services">The services to be run.</param>
        /// <param name="programArgs">The string array of arguments from Main method.</param>
        public static void Run(string[] programArgs, params ExecutableServiceBase[] services) {
            foreach (var service in services) service.programArgs = (string[]) programArgs.Clone();
            bool runAsService;
            bool debug = false;
#if DEBUG
            debug = true;
#endif
            runAsService = !(debug && Environment.UserInteractive);

            // if testing, run as console app, otherwise run as a Windows service
            if (runAsService) {
                ServiceBase.Run(services);
            } else {
                RunAsConsoleApp(services);
            }
        }

        /// <summary>
        /// Run the service within a console application.
        /// </summary>
        /// <param name="services">The services to be run.</param>
        private static void RunAsConsoleApp(ExecutableServiceBase[] services) {
            // for simpler testing, run as a console app
            Console.WriteLine("Starting services");
            foreach (var service in services) {
                service.OnStart(null);
            }
            Console.WriteLine("All services started");
            Console.WriteLine("Press P to Pause, C to Continue, S to Stop, D to Shutdown");
            Console.WriteLine("  PowerEvents: 0-BatteryLow, 1-OEM, 2-PowerStatusChange, 3-QuerySuspend,");
            Console.WriteLine("  4-QuerySuspendFailed,5-ResumeAutomatic, 6-ResumeCritical, 7-ResumeSuspend,");
            Console.WriteLine("  8-Suspend");
            bool running = true;
            while (running) {
                var key = Char.ToUpperInvariant(Console.ReadKey(false).KeyChar);
                Console.WriteLine();
                switch (key) {
                    case 'P':
                        foreach (var service in services) if (service.CanPauseAndContinue) service.OnPause();
                        break;
                    case 'C':
                        foreach (var service in services) if (service.CanPauseAndContinue) service.OnContinue();
                        break;
                    case 'S':
                        foreach (var service in services) if (service.CanStop) service.OnStop();
                        running = false;
                        break;
                    case 'D':
                        foreach (var service in services) if (service.CanShutdown) service.OnShutdown();
                        running = false;
                        break;
                    case '0':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.BatteryLow);
                        break;
                    case '1':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.OemEvent);
                        break;
                    case '2':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.PowerStatusChange);
                        break;
                    case '3':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.QuerySuspend);
                        break;
                    case '4':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.QuerySuspendFailed);
                        break;
                    case '5':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.ResumeAutomatic);
                        break;
                    case '6':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.ResumeCritical);
                        break;
                    case '7':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.ResumeSuspend);
                        break;
                    case '8':
                        foreach (var service in services) if (service.CanHandlePowerEvent) service.OnPowerEvent(PowerBroadcastStatus.Suspend);
                        break;
                }
            }
            Console.WriteLine("Services have been stopped/shutdown. Press any key to exit...");
            Console.ReadKey();
        }

        /// <summary>Gets the arguments passed in through Program.Main method.</summary>
        protected string[] ProgramArgs {
            get { return this.programArgs; }
        }

        /// <summary>Gets or sets the maximum number of seconds to wait for the service to stop cleanly.</summary>
        protected int MaxWaitSeconds {
            get { return this.maxWaitSecs; }
            set { this.maxWaitSecs = value; }
        }

        /// <summary>
        /// Gets a value indicating whether this ExecutableService is
        /// running as a Windows service. A value of false will be
        /// returned if this ExecutableService is running as a console
        /// application.
        /// </summary>
        public bool IsRunningAsService {
            get {
                return ((this.ServiceHandle != IntPtr.Zero)
                        || !Environment.UserInteractive);
            }
        }

        /// <summary>
        /// Write an entry to the event log if running as a servicer, or to the
        /// Console if running as a console application.
        /// </summary>
        /// <param name="message">The message to log.</param>
        /// <param name="entryType">The EventLogEntryType.</param>
        public virtual void WriteLogEntry(string message, EventLogEntryType entryType) {
            WriteLogEntry(message, entryType, 0);
        }

        /// <summary>
        /// Write an entry to the event log if running as a servicer, or to the
        /// Console if running as a console application.
        /// </summary>
        /// <param name="message">The message to log.</param>
        /// <param name="entryType">The EventLogEntryType.</param>
        /// <param name="eventId">The event Id.</param>
        public void WriteLogEntry(string message, EventLogEntryType entryType, int eventId) {
            if (IsRunningAsService) {
                WriteEventLogEntry(message, entryType, eventId);
            } else {
                Console.WriteLine(this.ServiceName + "(" + entryType.ToString() + ", Id=" + eventId + "): " + message);
            }
        }

        /// <summary>
        /// Write an entry to the event log.
        /// </summary>
        /// <param name="message">The message to log.</param>
        /// <param name="entryType">The EventLogEntryType.</param>
        /// <param name="eventId">The application-specific identifier for the event.</param>
        public virtual void WriteEventLogEntry(string message, EventLogEntryType entryType, int eventId) {
            EventLog.WriteEntry(message, entryType, eventId);
        }
    }
}

Normally, when you write a Windows service, you create a service class that descends from ServiceBase. To use the little service framework presented in this post, you make your service class inherit from ExecutableService instead. ExecutableService descends from ServiceBase, so you will get all of ServiceBase’s behavior. ExecutableService adds to that some additional behavior that deals with the fact that the service could be running as a service or a console app. In addition to a property that tells you if it's running as a service, there are two WriteLogEntry methods that can be used for basic logging. If the ExecutableService is being run as a service, the log messages are directed to the Windows event log. If it's being run as a console app, they are directed instead to standard output.
To use this class, include it in your service project, modify your program class as shown at the beginning of this post, change your service classes to descend from ExecutableServiceBase, and finally, set the Output type to Console Application on the Application tab of your service project properties.

In the next post I'll begin covering some of the other classes in this framework that build upon the ExecutableServiceBase class.

No comments:

Post a Comment