20120609

Generic Windows Service Host

Windows services are pretty useful, but debugging them from visual studio can be kind of a pain and repeating all the service and installer guts in every one gets a little old if you have to do very many of them.

Below is a console app that hosts a library as a service.  It can also run the service as a console app so you can F5 debug straight from studio and it has the ability to install and uninstall the service.

using System;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
using System.Linq;
using System.Reflection;
using System.ServiceProcess;
using thisproject.Host;
using System.Diagnostics;
 
namespace ConsoleServiceBase
{
 
    //this file is in the order it is to prevent you from using the designer.  do not reorder.  C# insits this is a designer file incorrectly, but it wasn't worth switching to VB to correct that
 
    /// 
    /// Static class that allows the executable to locate the library to run that is a descendant of ConsoleServiceLibrary
    /// 
    internal static class ServiceWorkLibraryFinder
    {
        private static Type LibraryType;
 
        internal static Type FindWork()
        {
            if (LibraryType == null)
            {
                foreach (FileInfo f in (new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory)).GetFiles("*.dll"))
                {
                    var a = Assembly.LoadFrom(f.FullName);
                    LibraryType = a.GetTypes().FirstOrDefault(x => x.IsSubclassOf(typeof(ConsoleServiceLibrary)));
                    if (LibraryType != nullbreak;
                }
            }
            return LibraryType;
        }
    }
 
    //do not move this class to the top of the file - the designer is not to be used on this class
 
    /// 
    /// Service Installer class for the /install /uninstall command line options of the ServiceConsole
    /// 
    [RunInstaller(true)]
    public class CustomServiceInstaller : Installer
    {
        public CustomServiceInstaller()
        {
            var WorkType = ServiceWorkLibraryFinder.FindWork();
 
            ServiceProcessInstaller serviceProcessInstaller = new ServiceProcessInstaller();
            ServiceInstaller serviceInstaller = new ServiceInstaller();
 
            //# Service Account Information
            serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
            serviceProcessInstaller.Username = null;
            serviceProcessInstaller.Password = null;
 
            //# Service Information
            serviceInstaller.DisplayName = WorkType.FullName;
            serviceInstaller.StartType = ServiceStartMode.Automatic;
            serviceInstaller.ServicesDependedOn = ((ConsoleServiceLibrary)Activator.CreateInstance(WorkType)).ServicesDependedOn;
            serviceInstaller.ServiceName = WorkType.FullName;
 
            this.Installers.Add(serviceProcessInstaller);
            this.Installers.Add(serviceInstaller);
        }
    }
 
 
    /// 
    /// ServiceConsole is a console app that can also run as a windows service.  It does no meaningful work on its own but will run the first library that is a descendant of ConsoleServiceLibrary in its filesystem path when run.
    /// 
    class ServiceConsole : ServiceBase
    {
        private static Type WorkType = ServiceWorkLibraryFinder.FindWork();
        private static ConsoleServiceLibrary WorkInstance;
 
        public ServiceConsole()
        {
            this.ServiceName = WorkType.FullName;
            this.AutoLog = true;
            WorkInstance = (ConsoleServiceLibrary)Activator.CreateInstance(WorkType);
        }
 
        protected override void OnStart(string[] args)
        {
            WorkInstance.NonBlockingStart();
            base.OnStart(args);
        }
 
        protected override void OnStop()
        {
            WorkInstance.BlockingStop();
            base.OnStop();
        }
 
        // The serviceinstaller is missing the bit to set recovery options, this does it through sc.exe.  It will always restart on fail after 60 seconds.
        static void SetRecoveryOptions(string serviceName)
        {
            int xCode;
            using (var process = new Process())
            {
                var startInfo = process.StartInfo;
                startInfo.FileName = "sc";
                startInfo.WindowStyle = ProcessWindowStyle.Hidden;
 
                // tell Windows that the service should restart if it fails
                startInfo.Arguments = string.Format("failure {0} reset= 0 actions= restart/60000", serviceName);
 
                process.Start();
                process.WaitForExit();
 
                xCode = process.ExitCode;
 
                process.Close();
            }
 
            if (xCode != 0)
                throw new InvalidOperationException();
        }
 
 
        static void Main(string[] args)
        {
            if (Environment.UserInteractive)
            {
                if (args.Contains("/install"))
                {
                    (new AssemblyInstaller(Assembly.GetExecutingAssembly(), new string[] { AppDomain.CurrentDomain.BaseDirectory })).Install(null);
                    SetRecoveryOptions(WorkType.FullName);
                    (new ServiceController(WorkType.FullName)).Start(new string[] { });
                }
                else if (args.Contains("/uninstall"))
                {
                    try
                    {
                        (new ServiceController(WorkType.FullName)).Stop();
                    }
                    catch (Exception)
                    {
                    }
 
                    (new AssemblyInstaller(Assembly.GetExecutingAssembly(), new string[] { AppDomain.CurrentDomain.BaseDirectory })).Uninstall(null);
                }
                else
                {
                    //be a debuggable console app
                    Console.WriteLine("add /install to the command line to install and run as a service");
                    Console.WriteLine("add /uninstall to the command line to stop the service and uninstall");
                    Console.WriteLine("");
                    var p = new ServiceConsole();
                    p.OnStart(null);
                    Console.WriteLine("running - press enter to kill...");
                    Console.ReadLine();
                    p.OnStop();
                }
            }
            else
            {
                //be the service
                ServiceBase.Run(new ServiceConsole());
            }
        }
    }
}

If you compile and run the executable it will detect that you are in userspace and run as a console app.  If you run it with the /install flag it will set itself up to run as a service.  The /uninstall removes the service.  The easiest way to set this up in studio is to make a console project then replace the main program class with the code above IMHO.

This by itself doesn't really do anything,but the first code references a class called ConsoleServiceLibrary which looks like this:


namespace thisproject.Host
{
    /// 
    /// Inherit this class to create a library that can be run by the ConsoleServiceHost
    /// 
    public abstract class ConsoleServiceLibrary
    {
        /// 
        /// override this method with something that starts the process and immediately returns control to the host
        /// 
        public abstract void NonBlockingStart();
 
        /// 
        /// override this method with something that signals the worker process to stop then does not return until stoppage is complete
        /// 
        public abstract void BlockingStop();
 
        /// 
        /// override this to return an array of services this service depends on.  Names must be ServiceNames not the friendly DisplayNames
        /// 
        public abstract string[] ServicesDependedOn { get; }
    }
}


This class also goes in the consoleproject.  Once you have built the console project you can set it as an external reference to separate projects (I usually use dll projects for the descendants) then inherit and implement the abstract class.  When the secondary project is built you just run the console executable that gets put in the bin directory and it will pick up and run the first dll that has a ConsoleServiceLibrary implementation in the same directory.  For the F5 goodness you need to set the dll project to start the console app out of bin\debug manually.

There are a couple limitations in this version that I do not mind, but you may:
  • there can only be one service dll in the folder with the host exe
  • the descendant dll doesn't end up being able to use a normal app.config easily because of the names and the path issues services bring.
  • the descendant type name will be the service name
  • the service always installs to run as system
This approach is something I needed  to be able to debug easily, deploy with simple scripts and file copy, hide the service weirdness from some of my coworkers who are really more asp devs than internals devs, and to make a big set of services quickly that all behave the exact same way from the servicehost side of things.  I mostly worked this out through trial and error and reading the msdn docs and code blogs over the years so there are probably better ways of doing some of these things.

20120425

simple js geolocation test

Nothing really special here, js Geolocation pulling a static google map image as a very simple example to test some browser settings.  See the code below or run the example here.

You Are Here:<br />
<img id='gmpsimg' />

<script type="text/javascript">
 if (navigator.geolocation) {
  var timeoutVal = 10 * 1000 * 1000;
  navigator.geolocation.getCurrentPosition(
    gmp, 
    showerror,
    { enableHighAccuracy: true, timeout: timeoutVal, maximumAge: 0 }
  );
 }
 else {
  alert("Geolocation is not supported by this browser");
 }

 function gmp(position) {
  document.getElementById('gmpsimg').src = 
  'http://maps.googleapis.com/maps/api/staticmap?markers=color:blue%7Clabel:X%7C'
  + position.coords.latitude + ',' + position.coords.longitude +
  '&zoom=17&size=400x400&sensor=true&maptype=hybrid';
 }

 function showerror(error) {
  var errors = { 
    1: 'Permission denied',
    2: 'Position unavailable',
    3: 'Request timeout'
  };
  alert("Error: " + errors[error.code]);
 }
</script>

Firefox Feedly RSS option

If you use Firefox with a RSS button and want the default RSS page to offer a Feedly option here is what you need to do: go to the about:c...