Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Install a Service using a Script

0.00/5 (No votes)
11 Nov 2004 2  
How to install a service using a script instead of a Windows Installer MSI package.

Sample Image - InstallUtil.gif

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:

  • Your service executable and DLLs are stored in an arbitrary location (usually under C:\Program Files). The installation location is user-controlled so you cannot make too many assumptions about assembly and DLL locations.
  • By default, Windows Installer puts your DLLs in the GAC. Your DLLs become machine-global. You have to correctly manage version numbers (or make sure DLLs are backward compatible) to avoid version conflict problems.
  • Every so often, Windows Installer corrupts the GAC reference counts. Once corruption occurs, you can't uninstall your service. When trying to uninstall your service, you get an error like:
    Unable to uninstall: assembly is required by one 
                              or more applications Pending references:
    SCHEME: <WINDOWS_INSTALLER>ID: <MSI> DESCRIPTION:<Windows Installer>

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>
{
    /// <summary>

    /// This is a custom project installer.

    /// Applies a unique name to the service using the /name switch

    /// Sets user name and password using the /user and /password switches

    /// Allows the use of a local account using the /account switch

    /// </summary>

    [RunInstaller(true)]
    public class ScriptedInstaller : Installer
    {
        private ServiceInstaller serviceInstaller;
        private ServiceProcessInstaller processInstaller;

        public ScriptedInstaller()
        {
            processInstaller             = new ServiceProcessInstaller();
            serviceInstaller             = new ServiceInstaller();

            // Set some defaults

            processInstaller.Account     = 
                            System.ServiceProcess.ServiceAccount.User;
            serviceInstaller.StartType   = ServiceStartMode.Automatic;
            serviceInstaller.ServiceName = "MonitoredServiceExample";

            Installers.Add(serviceInstaller);
            Installers.Add(processInstaller);
        }

        #region Access parameters

        /// <summary>

        /// Return the value of the parameter in dicated by key

        /// </summary>

        /// <PARAM name="key">Context parameter key</PARAM>

        /// <returns>Context parameter specified by key</returns>

        public string GetContextParameter(string key) 
        {
            string sValue = "";
            try 
            {
                sValue = this.Context.Parameters[key].ToString();
            }
            catch 
            {
                sValue = "";
            }

            return sValue;
        }

        #endregion

        /// <summary>

        /// This method is run before the install process.

        /// This method is overriden to set the following parameters:

        /// service name (/name switch)

        /// account type (/account switch)

        /// for a user account user name (/user switch)

        /// for a user account password (/password switch)

        /// Note that when using a user account,

        /// if the user name or password is not set,

        /// the installing user is prompted for the credentials to use.

        /// </summary>

        /// <PARAM name="savedState"></PARAM>

        protected override void OnBeforeInstall(IDictionary savedState)
        {
            base.OnBeforeInstall(savedState);

            bool isUserAccount = false;
            
            // Decode the command line switches

            string name        = GetContextParameter("name");
            serviceInstaller.ServiceName = name;

            // What type of credentials to use to run the service

            // The default is User

            string acct        = GetContextParameter("account");
            
            if (0 == acct.Length) acct = "user";

            // Decode the type of account to use

            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;
            }

            // User name and password

            string username = GetContextParameter("user");
            string password = GetContextParameter("password");

            // Should I use a user account?

            if (isUserAccount) 
            {
                // If we need to use a user account,

                // set the user name and password

                processInstaller.Username = username;
                processInstaller.Password = password;
            }
        }

        /// <summary>

        /// Modify the registry to install the new service

        /// </summary>

        /// <PARAM name="stateServer"></PARAM>

        public override void Install(IDictionary stateServer)
        {
            RegistryKey system,
                //HKEY_LOCAL_MACHINE\Services\CurrentControlSet

                currentControlSet,
                //...\Services

                services,
                //...\<Service Name>

                service,
                //...\Parameters - this is where you can 

                //put service-specific configuration

                config; 

            base.Install(stateServer);

            // Define the registry keys

            // Navigate to services

            system              = Registry.LocalMachine.OpenSubKey("System");
            currentControlSet   = system.OpenSubKey("CurrentControlSet");
            services            = currentControlSet.OpenSubKey("Services");
            // Add the service

            service             = 
              services.OpenSubKey(this.serviceInstaller.ServiceName, true);
            // Default service description

            service.SetValue("Description", 
                                 "Example ScriptedService implementation");

            // Display the assembly image path

            // and modify to add the service name

            // The executable then strips the name out of the image

            Console.WriteLine("ImagePath: " + service.GetValue("ImagePath"));
            string imagePath = (string)service.GetValue("ImagePath");
            imagePath        += " -s" + this.serviceInstaller.ServiceName;
            service.SetValue("ImagePath", imagePath);
            // Create a parameters subkey

            config            = service.CreateSubKey("Parameters");

            // Close keys

            config.Close();
            service.Close();
            services.Close();
            currentControlSet.Close();
            system.Close();
        }

        /// <summary>

        /// Uninstall based on the service name

        /// </summary>

        /// <PARAM name="savedState"></PARAM>

        protected override void OnBeforeUninstall(IDictionary savedState)
        {
            base.OnBeforeUninstall(savedState);

            // Set the service name based on the command line

            serviceInstaller.ServiceName = GetContextParameter("name");
        }

        /// <summary>

        /// Modify the registry to remove the service

        /// </summary>

        /// <PARAM name="stateServer"></PARAM>

        public override void Uninstall(IDictionary stateServer)
        {
            RegistryKey system,
                //HKEY_LOCAL_MACHINE\Services\CurrentControlSet

                currentControlSet,
                //...\Services

                services,
                //...\<Service Name>

                service;
                //...\Parameters - this is where you can 

                //put service-specific configuration


            base.Uninstall(stateServer);

            // Navigate down the registry path

            system            = Registry.LocalMachine.OpenSubKey("System");
            currentControlSet = system.OpenSubKey("CurrentControlSet");
            services          = currentControlSet.OpenSubKey("Services");
            service           = 
               services.OpenSubKey(this.serviceInstaller.ServiceName,true);
            // Remove the parameters key

            service.DeleteSubKeyTree("Parameters");

            // Close keys

            service.Close();
            services.Close();
            currentControlSet.Close();
            system.Close();
        }
    }//end class

}//end namespace declaration

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here