Creating a .NET Installer

Add an Installer to your class | Install your Service | Start your Service

Overview

Sometimes there are advantages to installing a service using scripting rather than by creating a MSI installer package. The MSI Windows Installer puts your dlls in the GAC, so that they become machine-global and you have to correctly manage version numbers (or make sure dlls are backward compatible) to avoid version conflict problems. If you're still testing, this can be a huge pain.

The first step to creating your own installer (after writing your Service, obviously) is to create a batch file that calls the .NET 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.

A sample batch file:

	@echo off
	echo Installing service...
	echo.
	%WINDIR%\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe SagemMorphoInc.SMTP.exe
	echo.
	pause

InstallUtil takes the following extra parameters:
Option Description

/h[elp] or /?

Displays command syntax and options for the tool.

/help assemblypath or /? assemblypath

Displays any additional options recognized by individual installers within the specified assembly.

/LogFile=[filename]

Specifies the name of the log file where install progress is recorded. The default is assemblyname.InstallLog.

/AssemblyName assemblyName
[,Version=major.minor.build.revision]
[,Culture=locale]
[,PublicKeyToken=publicKeyToken]]

Specifies the name of an assembly. The assembly name must be surrounded by quotes and fully qualified with the version, culture, and public key token of the assembly.

For example, "myAssembly, Culture=neutral, PublicKeyToken=0038abc9deabfle5, Version=2.0.0.0" is a fully qualified assembly name.

/LogToConsole={true|false}

If true, displays output to the console. If false (the default), suppresses output to the console.

/ShowCallStack

Prints the call stack to the log if an exception occurs at any point during installation.

/u[ninstall]

Uninstalls an assembly. Unlike other options, /u applies to all assemblies regardless of where it appears on the command line.

If you try to run this script now, without adding a ProjectInstaller to your service, you'll see the following error message:

	No public installers with the RunInstallerAttribute.Yes attribute could be found in the assembly.
	Remove InstallState file because there are no installers.

Since you don't have an installer associated with your service, InstallUtil doesn't know what to do.

To add an installer to your class:

  1. In VS.Net, open the service file (in solution explorer) in design view. This will open a mostly empty page that basically says "nothing to see here, click this link to view the code for this service." This is because there is nothing visual about a service for the designer to display.

  2. Right click anywhere on the design page and select "Add Installer."



  3. A new class that extends Installer will be added to your project. Open it in the solution explorer. It has two items on its design page: one which is called by default "ServiceProcessInstaller1" (a ServiceProcessInstaller) and another called "ServiceInstaller1" (a ServiceInstaller). They both need to be customized before your service will successfully install. Set the properties for each of these items (i.e. display name, description, start type, etc.)

  4. "ServiceProcessInstaller1", a ServiceProcessInstaller, is used by InstallUtil.exe to write registry values associated with your service.
    Set the RunInstallerAttribute on the class to true. ??

    You will want to specify what account the service application will run under by setting the Account property to one of the ServiceAccount options.
  5. "ServiceInstaller1", a ServiceInstaller, is used by the installation utility to write registry values associated with the service to a subkey within the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services registry key. The service is identified by its ServiceName within this subkey. The subkey also includes the name of the executable or .dll to which the service belongs.

    There should be one ServiceInstaller instance for each service in the application.

Install your service

Once you have an installer class in your service project you should be able to install your service using the InstallUtil.exe utility.

If you've added an Installer to your project, but still get an error like the one below when you run the above batch file or use InstallUtil.exe at a command line:

	No public installers with the RunInstallerAttribute.Yes attribute could be found in the C:\Projects\SagemMorphoInc.SMTP.exe assembly.
	Remove InstallState file because there are no installers.

One thing to check is that you're using the correct version of InstallUtil.exe. There is a copy in each version of the .NET framework, and depending on how your paths are set a previous version might be being called. If you've compiled your service using the 2.0 Framework, you won't be able to use the 1.0 or 1.4 InstallUtil utility.

Check the line in your log that looks somewhat like this:

	Microsoft (R) .NET Framework Installation utility Version 1.0.3705.6018 
	Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. 

to ensure that you're using the right version of InstallUtil. If it's not correct, either set your Windows environment path to use the correct framework version for your service, or simply type the full path to the version of InstallUtil.exe that you need to use.

For a .Net 1.1 assembly...
C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\InstallUtil.exe

Or

For a .Net 2.0 assembly...
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe

Start your service

Now assuming that you did not get any errors when installing, you need to start your service, since InstallUtil.exe will not start your service by default.

Open the Windows Services menu in Control Panel -> Administrative Tools, or type services.msc into the Start -> Run box or at a DOS command window. In the Windows Services menu, find your service in the alphabetic list, right-click, and select "Start". If you don't know what your service is named, check your ProjectInstaller source code for a line like the following (it's created by default when you add an Installer to your project):

	this.serviceInstaller1.ServiceName = "QueueListenerService";

The service in the code above will appear in the Windows Service menu as "QueueListenerService". You can also start a service directly using the command net start followed by the service's name, for example net start QueueListenerService. This can be added to your batch install file after you install the service.

If, when you start your service, you get a pop-up saying that your service was "started and then stopped. Some services stop automatically when they have no work to do, for example Performance Logs and Alert services", first ensure that your service isn't suppose to stop automatically.

If your service isn't suppose to automatically stop, then most likely you are getting an exception in your OnStart event code. Windows services do not handle exceptions well during initialization. It is best to avoid any complex code in your startup routine. Also, I have found it very helpful to have a logging method in my services to help troubleshoot problems or track the activity of the service.

Here is an example of a log file method.

VB Code | C# Code
 
// Writes specified text to the log file.
private void WriteToLog( string TextToLog ) {

	System.IO.StreamWriter sw = null;

	// Define log file path and name. 
	string CurrentLogFilePath = AppDomain.CurrentDomain.BaseDirectory 
					  + @"\Log.txt";

	sw = new System.IO.StreamWriter( CurrentLogFilePath, true );

	// Write data to log file. 
	sw.WriteLine( TextToLog );
	sw.Flush();
	sw.Close();
}

Here is an example of how to use the WriteToLog function. Note that even though the "LoadSettings" procedure is enclosed in a Try/Catch block, the service will still fail to start if an exception occurs in this routine. Like I said before, Windows services do not tolerate exceptions during their initialization. At least this way the error will be logged.

VB Code | C# Code
 
protected override void OnStart( string[] args ) {

	// Record service startup to log file. 
	WriteToLog( "========= " + DateTime.Now + " =========" );
	WriteToLog( "Service is starting." );

	try {
            
		// Load settings from config file. 
		LoadSettings();
		WriteToLog("Settings loaded from configuration file.");
                 
	} catch ( Exception ex ) {
		WriteToLog( "ERROR: " + ex.Message 
				+ Environment.NewLine + ex.StackTrace );
	}
}