Download ServiceGui1.zip
Introduction
There are lots of reasons why you might want an interface for your service: monitoring service activities, running a test harness, providing ad hoc mock data to service consumers, exercising upstream interfaces, etc. A user interface can provide useful visibility into the operation of an otherwise obscure process.
Of course, everyone knows Windows Services can't have a user interface, or can they? In some situations you can actually coerce the service into an interactive mode, at least prior to Vista. See this article for a complicated technique that might work for you. However, as the author says, it's "usually not worth the effort." WTSSendMessage can also be used if all you need is a simple message box and response.
For a more elaborate interface, you could create a separate GUI controller app that communicates with the service over some IPC channel using a custom protocol. This will work, provided your service successfully reaches a state where it can communicate with your controller app. Attaching a debugger to the running service is possible, but it has limitations. And who wants to maintain two apps when (as we shall see) one is sufficient?
So let's take a step back and consider if we need the service to have a GUI, or if it would be sufficient for the service to appear to have a GUI. If so, this article will show you how to achieve this goal and meet the following requirements:
1. You will only have to create a single app.
2. The GUI and the service will not interfere with each other in any way, unless you want them to.
3. Service consumers will be unaware that the GUI is running (unless you tell them).
4. Even if the GUI is running, the exact same code will be used to respond to service requests.
Background
Windows creates a "session" for each logged in user. Starting with Vista, Session 0 is reserved for services and non-interactive user apps; users run in Session 1 and higher. The switch was mainly for security reasons, to prevent user apps from accessing services that could be running with privileges. To complete the isolation, processes running in session 0 are denied access to the graphics hardware. More details can be found here.
The Solution in a Nutshell
We'll start by creating a standard Windows Service app; Visual Studio makes this very easy. When the app runs, it will call ServiceBase.Run(new Service())
, just like any other service. Then we're going to add a Form
to the project. Check System.Environment.UserInteractive
(or, use a command line argument if you want more options) at startup, and if the app is running in interactive mode, call Application.Run(new Form1())
instead of ServiceBase.Run()
, then call OnStart()
. To satisfy requirement #2 above, first use a ServiceController
to stop the service if it's running. When the GUI exits, use a ServiceController
to restart the service if it was running before the GUI started. You'll be able to switch from service mode to GUI and back again, and the service consumers will probably never know.
The Solution, One Step at a Time
Step 1: Create the Service Project
There are lots of articles and tutorials on how to create a service. If you're new to this, here's a link to get you started.
Our goal for today is to create a simple Windows Service that can be augmented with additional code in the following steps. Here is the VS2015 Solution Explorer and Program.cs
for my service.
Step 2: Make the Service Do Something
For this article, my service will simply write a line to a file periodically. In a real service you would probably do something more interesting, such as listening for requests on a socket or message queue. I've placed the OnStart
and OnStop
methods in Program.cs
for convenience. They will be called both from Program.Main
and from ServiceGui1.OnStart/OnStop
.
static public class Program
{
static Timer theTimer;
static int lineNumber = 0;
static void Main()
{
ServiceBase.Run(new ServiceGui1());
}
static internal void OnStart(string[] args)
{
theTimer = new Timer(tickHandler, null, 1000, 2000);
}
static internal void OnStop()
{
theTimer.Change(0, Timeout.Infinite);
theTimer.Dispose();
}
static private void tickHandler(object state)
{
string line = string.Format("Line {0}{1}", ++lineNumber, Environment.NewLine);
File.AppendAllText(@"C:\ServiceGuiLog.txt", line);
}
}
Step 3: Add a Form to the Project
The form can be as simple or as complex as you like. My GUIs typically have a menu structure that allows me to set operational or test modes, run unit or integration tests, view requests and responses as they occur, send mock responses to service consumers, send mock requests upstream, and so on. For this article, a simple public ListBox
to view the file will suffice.
Step 4: Add Service Controller Methods
Usually you don't want the service and GUI running at the same time, for example when they listen on a port for socket connections. If the service is running, a ServiceController
will stop the service while the GUI is running, then re-start the service when the GUI exits. (Of course, if you start the service after the GUI is running, there will be problems, so don't do that, but it you must, then add an appropriate interlock mechanism.)
private static void DisableServiceIfRunning()
{
try
{
ServiceController sc = new ServiceController("ServiceGui1");
sc.Stop();
serviceWasRunning = true;
Thread.Sleep(1000); }
catch (Exception)
{
}
}
private static void EnableServiceIfWasRunning()
{
if (serviceWasRunning)
{
try
{
ServiceController sc = new ServiceController("ServiceGui1");
sc.Start();
}
catch (Exception)
{
}
}
}
Step 5: Integrate the Service and GUI Modes
Make the following changes in Program.cs
to select the correct mode. If there's a trick involved, this is it.
using System.Windows.Forms;
static Form1 theForm;
[STAThread]
static void Main(string[] args)
{
if (Environment.UserInteractive)
{
DisableServiceIfRunning();
OnStart(args);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
theForm = new Form1();
Application.Run(theForm);
OnStop();
EnableServiceIfWasRunning();
}
else
{
ServiceBase.Run(new ServiceGui1());
}
}
Modify tickHandler
as follows:
static private void tickHandler(object state)
{
string line = string.Format("Line {0}{1}", ++lineNumber, Environment.NewLine);
if (Environment.UserInteractive)
{
MethodInvoker action = () =>
{
theForm.listBox1.Items.Add(line);
theForm.listBox1.TopIndex = theForm.listBox1.Items.Count - 1;
};
theForm.BeginInvoke(action);
}
File.AppendAllText(@"C:\ServiceGuiLog.txt", line);
}
You should be able to build and run the app and see the GUI pop up.
Step 6: Install and Start the Service
I've included batch files to install and uninstall the service. They use the v4.0.30319 InstallUtil
; modify the batch files if you need to use a different version. Run services.msc
to verify the service was installed. Start it. Verify that lines are being written to the file.
Step 7: Start the GUI
If everything is working correctly, you should see lines appearing in the listbox. Verify that lines are still being written to the file. In other words, the app is still acting like a service.
Step 8: Exit the GUI
The service will be restarted. Verify that lines are still being written to the file.
It's Your Turn
And that's it. I hope you found this article interesting, and possibly even useful. You might be happy with ServiceGui
just the way it is, or you might already be thinking of some enhancements you'd like to see. Let me know what clever modifications you make.
History
02 January 2017: Initial Version