Windows Service developer GUI for install while in development mode
Introduction
This article presents a Windows utility for interacting with a Windows service during the coding phase for start/stop and install/uninstalling, get status of your Windows service rather than opening Visual Studio command prompt, typing in a long command for install/uninstall. Also demonstrates using a simple Windows Service starting a debug session.
Manual install and uninstall of a Windows Service
The following shows how to install a service using x86 InstallUtil.exe where the path, in this case, is from the author's machine and would change for you.
C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /i C:\Dotnet_Development\ KarenPayneService\bin\Debug\KarenPayneService.exe
The following shows how to uninstall the service above
C:\Windows\Microsoft.NET\Framework\v 4.0 . 30319 \InstallUtil.exe /u C:\Dotnet_Development\ KarenPayneService\bin\Debug\KarenPayneService.exe
Both commands would be placed into a separate batch file and executed from Windows Explorer. The following is an example that builds such a batch file for uninstalling a service (found within the solution source code provided).
public static void CreateManualUninstallBatchFile(string pServiceProjectFolder, string pExecutableName)
{
_hasException = false;
var frameworkPath = "C:\\Windows\\Microsoft.NET\\Framework";
var utilVersion = "";
var utilNamne = "InstallUtil.exe";
var result = Directory.GetFiles(frameworkPath, "installutil.exe", SearchOption.AllDirectories);
try
{
utilVersion = result
.Select(fileName => fileName.Replace(frameworkPath, "").Replace(utilNamne, "")
.Replace("\\", "").Replace("v", ""))
.LastOrDefault();
var executablePathName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory
.UpperFolder(4), pServiceProjectFolder, pExecutableName);
var uninstallCommand = $"{frameworkPath}\\v{utilVersion}\\{utilNamne} /uninstall {executablePathName}";
var sb = new StringBuilder();
sb.AppendLine("@Echo off");
sb.AppendLine("@cls");
sb.AppendLine(uninstallCommand);
sb.AppendLine("pause");
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"Uninstall.bat"),sb.ToString());
}
catch (Exception e)
{
_hasException = true;
LastException = e;
}
}
There are several issues:
- If the Windows service project location changes both batch files need to be updated.
- If the project Windows service project location changes working with a team, each developer may have a different folder structure which means each developer must update both batch files and if accidentally checked into source control this means any other developer who checks out the batch files must update them to their folder structure.
Manually check service status
Open Task Manager, select the service tab, find your service and check the status. Below we can see the service included with the source code for this article.
A remedy to manual processes
This is where this utility comes invaluable as checking the status of a service, starting/stopping and install/uninstall are all handled by simple button clicks along with the service status in the form’s status bar.
Setting up the utility for your service
- Copy the project to the solution folder of your service.
- Open app.config and update values as per below.
- Set ExecutableName to the executable name of your service.
- Set ServiceKnownName to your service name. To get the service name, single click on the service which pulls up the service canvas, select properties which show the service name under the property Name.
- Set ServiceProjectFolder to the service folder, just the folder name as there is code in the class DirectoryExtensions to figure out where the service project is.
- Build the service.
- Build this utility.
Testing the included service
- Make sure SQL-Server is installed and running.
- In the class Operations, change the SQL-Server connection properties to match your SQL-Server install e.g. if SQL-Server Express is installed change the property database server from the current value to .\SQLEXPRESS while if using a named instance use this.
- In the project, KarenPayneService, run Createdatabase.sql and note lines 6 and 8 may need to change dependent on the path SQL-Server is installed.
This example service works by time and when triggered will write to the SQL-Server database then recycle to the timer. Note a System.Threading.Timer is used, not a Windows forms Timer.
If there are exceptions thrown they are written to the system events so make sure you have permission to write to events as this code assumes you do as a developer.
Running the utility
For installing (or installing without start service), start in Visual Studio and note the “Start Service” button and “Start with install” checkbox is disabled. Disabling these controls is done because if not disabled your debug session must start in a new instance of Visual Studio. When running the utility from Windows Explorer a prompt appears where one option is to debug with the current instance of Visual Studio which has your service project open.
Note there is a checkbox at the bottom of the window when checked keeps the utility above all other windows which some developers may prefer so the utility is always available while others may find this unwanted.
Rather than opening Windows Explorer to where the utility executable resides and clicking on it.
- From the IDE tool menu select “external tools”
- Select the “Add” button.
- Select the “…” button for Command, traverse to the executable folder for the utility and select it.
- Initial Directory to the path to the executable.
- Provide a title.
- Save.
Debugging a service
Locate where to start debugging in code and place the following code, no breakpoints are required. This code will not execute when setup for release. In the screenshot below the "Install Service" button has been pressed along with checking "Start with install" check box.
Note the instance of Visual Studio selected in the solution for the service installer. If you don't select this instance a new instance will start which takes time and is not warranted,
The following is where debugging will start.
Issues with Windows 10 UAC
If the following message appears
Disable Security Warning: Attaching to a process owned by an untrusted user can be dangerous . . .
This means Visual Studio in short is attempting to protect the developer.
The correct method to debug without UAC on Windows 10 is to disable UAC as explained in the following article.
Issues when installing/uninstalling a service
InstallUtil.exe creates a log which is a text file to examine for issues. Since this utility uses InstallUtil.exe it spans a command prompt where you can see what is going on during an install or uninstall so this coupled with the log file provides useful information.
Source code for installing/uninstalling services
The following class provides methods to control a service.
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
namespace ServiceInstaller.Classes
{
public class WindowsServices
{
/// <summary>
/// Stop a Windows service service name
/// </summary>
/// <param name="pServiceName"></param>
/// <remarks>
/// A service does not stop instantly, so WaitForStatus method
/// is used to 'wait' until the service has stopped. If the
/// caller becomes unresponsive then there may be issues with
/// the service stopping outside of code.
/// </remarks>
public void StopService(string pServiceName)
{
var sc = ServiceController.GetServices()
.FirstOrDefault(service => service.ServiceName == pServiceName);
if (sc == null)
return;
if (sc.Status == ServiceControllerStatus.Running)
{
try
{
sc.Stop();
sc.WaitForStatus(ServiceControllerStatus.Stopped);
}
catch (InvalidOperationException)
{
// here for debug purposes
}
}
}
/// <summary>
/// Start a Windows service by service name
/// </summary>
/// <param name="pServiceName"></param>
public void StartService(string pServiceName)
{
var sc = ServiceController.GetServices()
.FirstOrDefault(service => service.ServiceName == pServiceName);
if (sc == null)
return;
sc.ServiceName = pServiceName;
if (sc.Status == ServiceControllerStatus.Stopped)
{
try
{
sc.Start();
sc.WaitForStatus(ServiceControllerStatus.Running);
}
catch (InvalidOperationException)
{
// here for debug purposes
}
}
}
/// <summary>
/// Determine if service is currently installed
/// </summary>
/// <param name="pServiceName"></param>
/// <returns></returns>
public bool IsInstalled(string pServiceName)
{
var sc = ServiceController.GetServices()
.FirstOrDefault(service => service.ServiceName == pServiceName);
return (sc != null);
}
/// <summary>
/// Get basic information on running services
/// </summary>
/// <returns></returns>
public List<ServiceDetails> ServiceNames()
{
var detailList = new List<ServiceDetails>();
var services = ServiceController.GetServices()
.OrderBy(x => x.DisplayName).ToList();
foreach (var item in services)
{
detailList.Add(new ServiceDetails()
{
DisplayName = item.DisplayName,
ServiceName = item.ServiceName,
Status = item.Status
});
}
return detailList;
}
/// <summary>
/// provides the service status by string
/// </summary>
/// <param name="pServiceName"></param>
/// <returns></returns>
/// <remarks>
/// Example usage, set the text of a text box
/// in a form statusbar.
/// </remarks>
public string Status(string pServiceName)
{
var status = "Not installed";
// Get our service, if not found in GetServices then it's not installed
var sc = ServiceController.GetServices()
.FirstOrDefault(service => service.ServiceName == pServiceName);
if (sc == null)
return status;
switch (sc.Status)
{
case ServiceControllerStatus.Running:
status = "Running";
break;
case ServiceControllerStatus.Stopped:
status = "Stopped";
break;
case ServiceControllerStatus.Paused:
status = "Paused";
break;
case ServiceControllerStatus.StopPending:
status = "Stopping";
break;
case ServiceControllerStatus.StartPending:
status = "Starting";
break;
default:
status = "Status Changing";
break;
}
return status;
}
}
}
The following class provides methods to install/uninstall a service.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace ServiceInstaller.Classes
{
/// <summary>
/// A generic window service installer for install or uninstalling.
/// </summary>
public class Serviceinstaller
{
/// <summary>
/// Location of .NET Framework main folder
/// </summary>
public string FrameWorkDirectory => RuntimeEnvironment.GetRuntimeDirectory();
/// <summary>
/// Windows Service installer executable
/// </summary>
public string InstallerCommand => Path.Combine(FrameWorkDirectory, "InstallUtil.exe");
private bool _CommandExeExists;
public bool CommandExecutableExists { get { return _CommandExeExists; } }
public string ServiceFolder { get; set; }
/// <summary>
/// Executable name of the ACED Notification Service { get; set; }
/// </summary>
public string ServiceExecutableName { get; set; }
public bool ServiceExecutableExists { get; set; }
public bool ProceedWithOperations { get; set; }
/// <summary>
/// This is the Service name in the ProjectInstaller class of the window service
/// we are working with, in this case database Service.
///
/// Setup and determine if all is in place e.g.found the service executable, service installer executable
/// </summary>
public string ServiceName { get; set; }
/// <summary>
/// Used to install a service
/// </summary>
/// <param name="pExecutableName">Service executable name with extension</param>
/// <param name="pServiceKnownName">Name of the Windows service</param>
/// <param name="pServiceProjectFolder">Visual Studio project folder</param>
public Serviceinstaller(string pExecutableName, string pServiceKnownName, string pServiceProjectFolder)
{
ServiceExecutableName = pExecutableName;
ServiceName = pServiceKnownName;
_CommandExeExists = File.Exists(InstallerCommand);
// assumes building for debug not release
ServiceFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.UpperFolder(4),
pServiceProjectFolder, "bin" , "Debug");
ServiceExecutableExists = File.Exists(Path.Combine(ServiceFolder, ServiceExecutableName));
ProceedWithOperations = CommandExecutableExists && ServiceExecutableExists;
}
/// <summary>
/// Used to uninstall a service.
/// </summary>
public void UninstallService()
{
var startInfo = new ProcessStartInfo(InstallerCommand) {WindowStyle = ProcessWindowStyle.Normal};
var ops = new WindowsServices();
var statusStates = new [] { "Running" , "Stopped" };
if (statusStates.Contains(ops.Status(ServiceName)))
{
startInfo.Arguments = $ "/u {Path.Combine(ServiceFolder, ServiceExecutableName)}" ;
var p = Process.Start(startInfo);
p.WaitForExit();
if (ops.Status(ServiceName) == "Not installed")
{
MessageBox.Show( "Service has been uninstalled" );
}
}
}
/// <summary>
/// Start our service.
/// First check to see if the service is running as attempting
/// to install while running leads to doom and gloom :-)
/// Install after the above check
/// </summary>
public void InstallService()
{
var startInfo = new ProcessStartInfo(InstallerCommand) {WindowStyle = ProcessWindowStyle.Normal};
var ops = new WindowsServices();
if (ops.Status(ServiceName) == "Running")
{
startInfo.Arguments = $ "/u {Path.Combine(ServiceFolder, ServiceExecutableName)}" ;
Process.Start(startInfo);
}
startInfo.Arguments = $ "/i {Path.Combine(ServiceFolder, ServiceExecutableName)}" ;
Process.Start(startInfo);
}
}
}
Requirements
- Microsoft Visual Studio 2015 or higher (originally written in VS2015 and presented here using VS2017).
- Microsoft SQL-Server Express or full edition.
Source code
https://github.com/karenpayneoregon/WindowsInstaller
See also
How to Install/Uninstall .NET Windows Service [C#]
How to Grant Users Rights to Manage Services (Start, Stop, Etc.)
Summary
In this article, a utility has been presented to easily automate the installing/uninstalling a Windows Service along with the current status of a service and the ability to start and stop a Windows Service. By using this utility a developer can spend more time on the actual service then being concerned with the install and uninstall processes of the service,