Click here to Skip to main content
16,017,297 members
Articles / Desktop Programming / Win32

SelfServe: A Self-hosting Self-installing Windows Service

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
14 Jan 2020MIT6 min read 19.6K   250   36   14
Add the ability to run a service in console mode and to control or install your service from the command line

Introduction

I don't like installers. I don't like that services must be installed in order to be used, and so they require admin permissions no matter what they do. This is why I write my services to be self hosting and self installing. I'm about to show you what that looks like, and what it means for your code and for its users.

Conceptualizing this Mess

Windows services are the somewhat heavier handed Microsoft answer to POSIX daemons. They allow you to run a background process that performs some work and typically exists for the life of the OS session. In order to use them, rather than simply running it from the console, one must first install the service and then start it through the control panel (assuming it's not set to start automatically, although almost all services are). Installing, as usual requires administrator permissions, since it's system wide.

SelfServe will run a service in one of two different modes: Either in "interactive mode" where the service can be started on a per user-session basis, runs in the context of the current command prompt, and blocks until stopped, or in non-interactive mode, when installed. In the latter case, this runs like a normal windows service, and is a background process that does not block the command window.

SelfServe enforces singleton semantics. That is, only one instance of the service may be running at once; If the background windows service is running, no interactive mode service can be started. If an interactive mode service is running, the service cannot be started. Note that it's still possible in the case of multiple users to have multiple instances of the service running. In addition to hosting the service, SelfServe is also used as a controller app to start, stop, install, uninstall, and check the status of the service.

Despite all this, using it is simple:

Usage: SelfServe.exe /start | /stop | /install | /uninstall | /status

   /start      Starts the service, if it's not already running. When not installed, 
               this runs in console mode.
   /stop       Stops the service, if it's running. This will stop the installed service, 
               or kill the console mode service process.
   /install    Installs the service, 
               if not installed so that it may run in Windows service mode.
   /uninstall  Uninstalls the service, if installed, 
               so that it will not run in Windows service mode.
   /status     Reports if the service is installed and/or running.
  • /start - when installed as a Windows service, this will start the service. When not installed, this will start the service in console mode, if it's not already running.
  • /stop - when installed as a Windows service, this will stop the service. When not installed, this will stop any instance already running for this user.
  • /install - installs SelfServe as a Windows service. Running this requires admin permissions.
  • /uninstall - uninstalls SelfServe as a Windows service. Running this requires admin permissions.

This hides an awful lot of complexity behind an easy facade. I had to jump through numerous hoops to get this to work.

Coding this Mess

Because of what it does, this project cannot be distributed as a library. Instead, to make your own service with this codebase, simply copy Program.cs and Service.cs to your own service project, and then be sure to set the properties on the service component, particularly the ServiceName property (not to be confused with Name). There's not really a demo code block to show for that. The source code itself is the demo.

With that in mind, let's explore how I coded it instead of how to code against it.

Getting Service Properties

In order to get the properties for the service - really the ServiceName property, I simply create a temporary instance of the service class and then read its properties. This is so you, the developer, only have to set the properties in one place - in the designer on the service itself. Otherwise, we'd have to make our own mechanism for defining the service name. If we then are to start the service, I simply recycle that instance. Otherwise, it gets thrown away when the process exits without ever running.

Singleton Semantics

I implemented this using a named mutex. Any time the service starts, whether in Windows service mode or in console mode, I create one of these, using the service name:

C#
bool createdNew = true;
using (var mutex = new Mutex(true, svctmp.ServiceName, out createdNew))
{
    if (createdNew)
    {
        mutex.WaitOne();
        // run code here...
    }
    else
        throw new ApplicationException("The service " + 
                  svctmp.ServiceName + " is already running.");
}

This ensures that only one instance can be started at a time.

Detecting When Starting as a Windows Service

The mechanism we use for determining whether the app is being run from the command line or as a Windows service is quite simple. We just check the Environment.UserInteractive property. If true, the app is being run from the command line. Otherwise, it's being run as a Windows service.

Starting in Windows Service Mode

In Windows service mode, we simply start the service like normal:

C#
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
    svctmp
};
ServiceBase.Run(ServicesToRun);

Starting in Console Service Mode

In console mode, running the service is slightly more involved, as we need to host the service class ourselves:

C#
var type = svc.GetType();
var thread = new Thread(() =>
{
    // HACK: In order to run this service outside of a service context, 
    // we must call the OnStart() protected method directly
    // so we reflect
    var args = new string[0];
    type.InvokeMember("OnStart", BindingFlags.InvokeMethod | 
         BindingFlags.NonPublic | BindingFlags.Instance, null, svc, new object[] { args });
    while (true)
    {
        Thread.Sleep(0);
    }
});
thread.Start();
thread.Join();

// probably never run, but let's be sure to call it if it does
type.InvokeMember("OnStop", BindingFlags.InvokeMethod | 
                   BindingFlags.NonPublic | BindingFlags.Instance, null, svc, new object[0]);

Here, you can see things got a little hairy. The main thing to note is we're using reflection! The reason we have to is because we can't call service.Start() to start the service because it isn't installed as a windows service. Instead, we just want to pump OnStart() directly so that the service's initialization code gets run, but that method is protected. The same thing is true of OnStop(). In addition, we're doing all this on a separate thread. That wasn't strictly necessary, but I wanted to give the service its own thread context other than the main app thread. In that thread, after calling OnStart(), we simply spin forever waiting for the process to die. This keeps the service alive until the process is killed (typically via another SelfServe instance being run with /stop).

Stopping the Service

Stopping the service happens in one of two ways depending on whether it is a Windows service or not. If it's a Windows service, the service is stopped. Otherwise, each user process is enumerated and any matching this process name other than this process itself are killed.

C#
static void _StopService(string name, bool isInstalled)
{
    if (isInstalled)
    {
        ServiceInstaller.StopService(name);
    }
    else
    {
        var id = Process.GetCurrentProcess().Id;
        var procs = Process.GetProcesses();
        for (var i = 0; i < procs.Length; ++i)
        {
            var proc = procs[i];
            var f = proc.ProcessName;
            if (id != proc.Id && 0 == string.Compare
                                 (Path.GetFileNameWithoutExtension(_File), f))
            {
                try
                {
                    proc.Kill();
                    if (!proc.HasExited)
                        proc.WaitForExit();
                }
                catch { }
            }
        }
    }
    _PrintStatus(name);
}

Installing and Uninstalling

My initial effort involved attempting to use ServiceInstaller directly to drive InstallUtil.exe but there were a couple of problems with that approach. First, it wasn't working, as it wanted some state that is undocumented or at least not anywhere I could find. I was getting errors when trying to call Install(), whether with args or without. Second, some systems may not have InstallUtil.exe in the first place, in which case this will fail anyway.

Unfortunately, instead I had to implement native calls into advapi32.dll to install or uninstall the service. Stack Overflow had a surprisingly good implementation of a ServiceInstaller class here - (provided by Lars A. Brekken - original author unknown), so I just used that.

I also use this class to start and stop the Windows service. Using it is simple, as we do here:

C#
static void _InstallService(string name)
{
    var createdNew = true;
    using (var mutex = new Mutex(true, name, out createdNew))
    {
        if (createdNew)
        {
            mutex.WaitOne();
            ServiceInstaller.Install(name, name, _FilePath);
            Console.Error.WriteLine("Service " + name+ " installed");
        }
        else
        {
            throw new ApplicationException("Service " + name+ " is currently running.");
        }
    }
}

Note that I created a mutex here. While I haven't avoided every possible race condition, this attempts to avoid one of them by "locking" the app from installing while started or starting while installing.

Enabling in Your Own Projects

Remember, copy Program.cs and Service.cs to your own project. Add a reference to System.ServiceProcess and set the ServiceName on the Service component through the Properties panel. Then simply add your service code to Service.cs.

History

  • 14th January, 2020 - Initial submission

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
United States United States
Just a shiny lil monster. Casts spells in C++. Mostly harmless.

Comments and Discussions

 
QuestionHandling self-installation Pin
William Wade 202117-Jan-24 5:08
William Wade 202117-Jan-24 5:08 
AnswerRe: Handling self-installation Pin
honey the codewitch17-Jan-24 5:10
mvahoney the codewitch17-Jan-24 5:10 
QuestionNicely done Pin
William Wade 202117-Jan-24 4:40
William Wade 202117-Jan-24 4:40 
QuestionI like your approach Pin
MSBassSinger16-Jan-20 8:09
professionalMSBassSinger16-Jan-20 8:09 
AnswerRe: I like your approach Pin
honey the codewitch16-Jan-20 10:51
mvahoney the codewitch16-Jan-20 10:51 
GeneralRe: I like your approach Pin
MSBassSinger16-Jan-20 11:05
professionalMSBassSinger16-Jan-20 11:05 
GeneralRe: I like your approach Pin
honey the codewitch16-Jan-20 11:17
mvahoney the codewitch16-Jan-20 11:17 
GeneralRe: I like your approach Pin
MSBassSinger16-Jan-20 11:22
professionalMSBassSinger16-Jan-20 11:22 
PraiseRe: I like your approach Pin
stixoffire1-Feb-20 5:02
stixoffire1-Feb-20 5:02 
GeneralRe: I like your approach Pin
honey the codewitch17-Jan-24 5:13
mvahoney the codewitch17-Jan-24 5:13 
GeneralRe: I like your approach Pin
stixoffire1-Feb-20 4:59
stixoffire1-Feb-20 4:59 
GeneralRe: I like your approach Pin
honey the codewitch1-Feb-20 5:23
mvahoney the codewitch1-Feb-20 5:23 
Questiongreat service! Pin
Jan Heckman16-Jan-20 0:49
professionalJan Heckman16-Jan-20 0:49 
AnswerRe: great service! Pin
honey the codewitch16-Jan-20 2:24
mvahoney the codewitch16-Jan-20 2:24 
It wasn't the person's original code at Stack overflow either.

I wasn't going to do google forensics to find out where it originally came from, but i went ahead and added the name of the person that provided it second hand.
Real programmers use butterflies

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.