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:
bool createdNew = true;
using (var mutex = new Mutex(true, svctmp.ServiceName, out createdNew))
{
if (createdNew)
{
mutex.WaitOne();
}
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:
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:
var type = svc.GetType();
var thread = new Thread(() =>
{
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();
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.
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:
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