Introduction
Many believe that an MSI install file is the only way to install a Windows service. In truth, using an MSI is only one of several ways to install a Windows service. This article will explain how to perform a scripted install of a Windows service. In addition to explaining how to install a service using a script, this article will also explain how to install multiple instances of the same assembly as multiple Windows services.
Background
I maintain a distributed software application. The application back-end requires multiple services running on multiple servers. Each server hosts 15-25 services.
Creating an MSI file for each service and then manually running Windows Installer for 25 services on each machine is a terrible burden on my IT OPS team.
The solution I use is 'XCOPY deployment'. To install, just unzip (or copy from CD) the entire directory tree (containing all services), and then run an install script to register all services. To uninstall � run the uninstall script and delete the directory tree.
In my experience, using the XCOPY deployment is much quicker and requires far less work than using individual Windows Installer MSI files. Yes, there is a way to bundle multiple MSI files into one 'master' package, but keep reading and you will understand why scripted install is so much better. For additional information about XCOPY deployment, see Determining When to Use Windows Installer Versus XCOPY.
The problem with MSI
Installing a service using an MSI file can be very convenient if you install a very limited set of services (one to four). When using an MSI file, you may encounter the following problems:
If you have already encountered this problem, you can find a solution here.
Using XCOPY deployment, you avoid the Windows Installer GAC corruption problem, and get to keep private versions of your DLLs in each service executable directory.
Scripted install basics
Scripted installs use the .NET SDK InstallUtil.EXE utility. InstallUtil invokes the ProjectInstaller
module in your assembly. For installation, InstallUtil monitors all installation steps and rolls back the installation if an error occurs. For uninstalling, InstallUtil runs the Uninstall code in your ProjectInstaller
module. For more information about the install process, see MSDN documentation for the ServiceInstaller class.
To use scripted install, you need three pieces of code:
- A project installer module added to your assembly
- A short installation script
- A short un-installation script
The Project Installer Module
This is ProjectInstaller.cs. A ProjectInstaller.vb is available in the Source files package thanks to David Wimbush.
using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.Collections.Specialized;
using System.ServiceProcess;
using Microsoft.Win32;
namespace <Your namespace here>
{
[RunInstaller(true)]
public class ScriptedInstaller : Installer
{
private ServiceInstaller serviceInstaller;
private ServiceProcessInstaller processInstaller;
public ScriptedInstaller()
{
processInstaller = new ServiceProcessInstaller();
serviceInstaller = new ServiceInstaller();
processInstaller.Account =
System.ServiceProcess.ServiceAccount.User;
serviceInstaller.StartType = ServiceStartMode.Automatic;
serviceInstaller.ServiceName = "MonitoredServiceExample";
Installers.Add(serviceInstaller);
Installers.Add(processInstaller);
}
#region Access parameters
public string GetContextParameter(string key)
{
string sValue = "";
try
{
sValue = this.Context.Parameters[key].ToString();
}
catch
{
sValue = "";
}
return sValue;
}
#endregion
protected override void OnBeforeInstall(IDictionary savedState)
{
base.OnBeforeInstall(savedState);
bool isUserAccount = false;
string name = GetContextParameter("name");
serviceInstaller.ServiceName = name;
string acct = GetContextParameter("account");
if (0 == acct.Length) acct = "user";
switch (acct)
{
case "user":
processInstaller.Account =
System.ServiceProcess.ServiceAccount.User;
isUserAccount = true;
break;
case "localservice":
processInstaller.Account =
System.ServiceProcess.ServiceAccount.LocalService;
break;
case "localsystem":
processInstaller.Account =
System.ServiceProcess.ServiceAccount.LocalSystem;
break;
case "networkservice":
processInstaller.Account =
System.ServiceProcess.ServiceAccount.NetworkService;
break;
default:
processInstaller.Account =
System.ServiceProcess.ServiceAccount.User;
isUserAccount = true;
break;
}
string username = GetContextParameter("user");
string password = GetContextParameter("password");
if (isUserAccount)
{
processInstaller.Username = username;
processInstaller.Password = password;
}
}
public override void Install(IDictionary stateServer)
{
RegistryKey system,
currentControlSet,
services,
service,
config;
base.Install(stateServer);
system = Registry.LocalMachine.OpenSubKey("System");
currentControlSet = system.OpenSubKey("CurrentControlSet");
services = currentControlSet.OpenSubKey("Services");
service =
services.OpenSubKey(this.serviceInstaller.ServiceName, true);
service.SetValue("Description",
"Example ScriptedService implementation");
Console.WriteLine("ImagePath: " + service.GetValue("ImagePath"));
string imagePath = (string)service.GetValue("ImagePath");
imagePath += " -s" + this.serviceInstaller.ServiceName;
service.SetValue("ImagePath", imagePath);
config = service.CreateSubKey("Parameters");
config.Close();
service.Close();
services.Close();
currentControlSet.Close();
system.Close();
}
protected override void OnBeforeUninstall(IDictionary savedState)
{
base.OnBeforeUninstall(savedState);
serviceInstaller.ServiceName = GetContextParameter("name");
}
public override void Uninstall(IDictionary stateServer)
{
RegistryKey system,
currentControlSet,
services,
service;
base.Uninstall(stateServer);
system = Registry.LocalMachine.OpenSubKey("System");
currentControlSet = system.OpenSubKey("CurrentControlSet");
services = currentControlSet.OpenSubKey("Services");
service =
services.OpenSubKey(this.serviceInstaller.ServiceName,true);
service.DeleteSubKeyTree("Parameters");
service.Close();
services.Close();
currentControlSet.Close();
system.Close();
}
}
}
Notice the [RunInstaller(true)]
line � this line marks the Project Installer as an installer and makes sure the code is accessible to InstallUtil (and to the MSI installer for that matter).
The constructor
The installer starts by instantiating a process and service installer and setting some defaults.
Runtime parameter access
The GetContextParameter
property allows access to the runtime parameters supplied by the InstallUtil utility.
Pre-installation
The OnBeforeInstall()
method is executed prior to the actual installation. Here, the runtime parameters passed by the InstallUtil utility are parsed and the installation attributes are set.
The service name is set using the /name
switch. Why not use the assembly name? Because, I may need to install multiple instances of the same assembly as different services. The concept of installing multiple instances of the same assembly probably deserves a separate article. For now, I refer you to the Unix inetd daemon.
After the service name is set, the service account type is determined. If the account type is a user account, the user and password information is obtained. The service is ready to be installed.
Installation
The Install()
method creates the service registry keys and computes the "imagepath"
which is the actual command line executed when the service is run. The service name is appended to the imagepath
so the service can determine what name it is running under.
Pre-Uninstallation
The OnBeforeUninstall()
method looks for the only relevant parameter - the service name, specified with the /name
switch.
Uninstallation
The Uninstall()
method removes the registry keys for the service from the registry.
The Install Script
This is installService.bat:
@echo off
set SERVICE_HOME=<service executable directory>
set SERVICE_EXE=<service executable name>
REM the following directory is for .NET 1.1, your mileage may vary
set INSTALL_UTIL_HOME=C:\WINNT\Microsoft.NET\Framework\v1.1.4322
REM Account credentials if the service uses a user account
set USER_NAME=<user account>
set PASSWORD=<user password>
set PATH=%PATH%;%INSTALL_UTIL_HOME%
cd %SERVICE_HOME%
echo Installing Service...
installutil /name=<service name>
/account=<account type> /user=%USER_NAME% /password=%
PASSWORD% %SERVICE_EXE%
echo Done.
The variables set at the top are for convenience only, you can certainly hardcode all information directly on the installutil
line.
The Uninstall Script
This is uninstallService.bat:
@echo off
set SERVICE_HOME=<service executable directory>
set SERVICE_EXE=<service executable name>
REM the following directory is for .NET 1.1, your mileage may vary
set INSTALL_UTIL_HOME=C:\WINNT\Microsoft.NET\Framework\v1.1.4322
set PATH=%PATH%;%INSTALL_UTIL_HOME%
cd %SERVICE_HOME%
echo Uninstalling Service...
installutil /u /name=<service name> %SERVICE_EXE%
echo Done.
The Parameters
Name
� sets the service name to the given string. /Name=MonitoredServiceExample
.
Account
� What type of credentials should the service use. Options are:
/account=user
� use a user account (the default), an account defined by a specific user on the network.
/account=localservice
� use a LocalService account, an account that acts as a non-privileged user on the local computer, and presents anonymous credentials to any remote server.
/account=localsystem
� use a LocalSystem account, an account that has a high privileged level.
/account=networkservice
� use a NetworkService account, an account that provides extensive local privileges, and presents the computer's credentials to any remote server.
User
� if the service is to run using a user account, specifies the name of the user. /user=MyDomain\Me
.
Password
� if the service is to run using a user account, specifies the password associated with the user account. /password=secret
.
Note that if the service is to run with user account credentials and either the user name or the password is not specified (or is incorrect), the installing user will be prompted for the user name and password to use during the installation.
The Example
The included example contains the source, project installer, and scripts for a simple service that does nothing. Compile in Debug mode, and use the install and uninstall scripts to install and remove the service. Amuse yourself by installing the service with different credentials: user
, network service
, local service
, etc�
Installing Multiple Instances of the Same Assembly
Sometimes, you need to install multiple instances of the same assembly as different services. I can think of several reasons:
- Have multiple instances of the service, each dedicated to a different client/purpose.
- Create a 'master' service that behaves differently based on the name it is given.
Yes, you could multi-thread a service instead of installing multiple instances, but there are advantages to having a separate memory space and being able to start and stop individual instances.
The modification to the install script is as follows: Instead of:
installutil /name=<service name> /account=<account
type> /user=%USER_NAME% /password=%PASSWORD% %SERVICE_EXE%
use
installutil /name=<first service name> /account=<account
type> /user=%USER_NAME% /password=%PASSWORD% %SERVICE_EXE%
installutil /name=<second service name> /account=<account
type> /user=%USER_NAME% password=%PASSWORD% %SERVICE_EXE%
and for the uninstall script, replace
installutil /u /name=<service name> %SERVICE_EXE%
with
installutil /u /name=<first service name> %SERVICE_EXE%
installutil /u /name=<second service name> %SERVICE_EXE%
Mind boggling, isn't it?
Allow Service to Interact with Desktop
The following uses undocumented Windows features to set the 'Interact with Desktop' bit. Use at your own risk. You can get some additional information here.
To get the 'interact with desktop' feature requires two steps:
- Add a new switch
- Add an
OnAfterInstall
method to the installer
After adding a new
bool
flag to the class, say
bool m_interactWithDesktop
add the following to the
OnBeforeInstall() method
protected override void OnBeforeInstall(IDictionary savedState)
{
base.OnBeforeInstall(savedState);
...
string interact = GetContextParameter("InteractWithDesktop");
if (interact.ToLower() == "true")
{
m_interactWithDesktop = true;
}
...
}
Add the new
OnAfterInstall()
method
protected override void OnAfterInstall(IDicionary savedState)
{
base.OnAfterInstall(savedState);
if (m_interactWithDesktop)
{
string keyPath = @"SYSTEM\CurrentControlSet\Services\" +
this.serviceInstaller.ServiceName;
RegistryKey ckey = Registry.LocalMachine.OpenSubKey(keyPath, true);
if ((null != ckey) && (null != ckey.GetValue("Type")))
{
ckey.SetValue("Type", (int)ckey.GetValue("Type") | 0x100);
}
}
}
The above cannot be guaranteed to work as the bit values for the 'Type' key are not published.
Possible improvements
Currently, the service name defaults to a hard-coded value if not specified. The name of the executing assembly may be a better default.
The service description is currently hard-coded. Service description could be passed in as a parameter, or an assembly property can be used.
SC.EXE
SC.EXE
is part of the Windows 2000 resource kit. The SC.EXE
utility allows you to add, delete and modify services even if you do not have access to the service assemblies or MSI files.
SC.EXE
is a great administration tool and very handy when you have to deal with emergencies. If you have lost the executable and have a bogus service definition in your registry - SC.EXE
is the tool for the job. Every system admin should have a collection of tools to help with maintenance, and SC.EXE is a good tool to have in your system admin toolbox.
You can find documentation about SC.EXE
on Microsoft support and more information on Vasudevan Deepak Kumars blog.
Note however that using a ProjectInstaller
and InstallUtil
have the following advantages over SC.EXE
:
- You can create/modify/delete customized registry entries in the
Install()
method of the ProjectInstaller
. SC.EXE
just creates/deletes the standard registry service keys.
- Using
ProjectInstaller
you can define your own command line switches for the service installation, passing in information that makes sense to you. There is no way to pass in optional information using SC.EXE
.
- While you can instantiate multiple instances of the same assembly using
SC.EXE
(by creating multiple services) there is no way to inform the running service under what name it is running.
- Having a
ProjectInstaller
class makes your service compatible with Windows Installer. If you want to install the same service using an MSI package, no changes are required.
- Removing a service using
SC.EXE
marks the service for deletion on reboot, removing with InstallUtil
is immediate as the registery changes are done as part of the uninstall.
Additional information about InstallUtil
You can find additional information about the InstallUtil utility on MSDN.
Version history