A couple of years ago, I wrote an article* explaining how to simplify implementation, debugging, and installation of Windows services written in C#. The approach described in the article worked for me very well, but recently I ran into an issue related to Windows Vista. I'm not sure whether it's a problem with Windows Installer (AKA Microsoft Installer, or MSI) or Visual Studio, but for some reason, the installer cannot install Windows services from Visual Studio-built MSI files when User Account Control (UAC) is enabled. In this post, I'll explain how to correct this problem and build a Windows service installer, which works on Vista, as well as pre-Vista versions of Windows (XP, Windows Server 2003, etc).

[Disclaimer: To install a Windows service, I use a custom actions (CA) implemented in a ServiceInstaller-based class. Some MSI gurus do not recommend this approach; however, this seems to be the direction currently advocated by Microsoft. If you can suggest a better option, please submit it in a comment.]

UPDATE: A better alternative to using custom actions (CA) implemented in a ServiceInstaller-based class, would be to define the Windows Service-related entries directly in the MSI package. Although, I'm not sure how to do this using Visual Studio Installer, it is rather trivial to do in WiX. For more info on WiX read my three-part series.

Windows service installation is a privileged operation which is restricted to administrative accounts. To install a Windows service on Vista (with enabled UAC), you can use one of the following methods.

Option 1: Disable UAC (not recommended)
You will probably not want to do this; at least, not for the sole purpose of solving the installation problems.

Option 2: Execute msiexec.exe as Administrator (not recommended)
This option is rather inconvenient because there is no shortcut to launch msiexec.exe as Administrator. You must also enter all command-line parameters passed to msiexec.exe by hand.

Option 3: Launch the installer using a bootstrapper (not recommended) According to Jonathan Wells, a product manager for Visual Studio,

"Windows Vista heuristically detects installation programs and requests administrator credentials or approval from the administrator user in order to run with access privileges. So applications called "setup.exe" and "install.exe" will prompt for administrator credentials (if running as standard user) or approval (if running as Administrator). If you do not desire this behaviour then include a manifest with your application."

This option is not recommended for a number of reasons. First, it requires a bootstrapper file to be deployed along with the MSI file. Second, the bootstrapper will need a manifest specifying the right requestedExecutionLevel (notice that you can embed manifest in the executable). Finally, depending on the policy configuration, installer detector responsible for determining whether a process is an installation program can be disabled (see the Installer Detection Technology section in Understanding and Configuring User Account Control in Windows Vista).

Option 4: Fix the MSI file (recommended)
If I understand it correctly, the problem is caused by the wrong value defined by Visual Studio for the service installer's custom action type. If you open the MSI file in Orca and look at the contents of the CustomAction table, you will see (among other records) three entries corresponding to the Install, Uninstall, and Commit custom actions, which look similar to the following:

Action

Type

Source

Target

_12345678_9ABC_DEF0_1234_56789ABCDEF1.uninstall

1025

InstallUtil

ManagedInstall

_FEDCBA98_7654_3210_FEDC_BA9876543210.install

1025

InstallUtil

ManagedInstall

_87654321_CBA9_0FED_4321_1FEDCBA98765.commit

1537

InstallUtil

ManagedInstall

The root of the issue is in the missing msidbCustomActionTypeNoImpersonate bit (2048) in the custom action types 1025 and 1537. You need to turn the msidbCustomActionTypeNoImpersonate bit on to let the custom actions run with a privileged token. You can manually change the type values using Orca, but a better option would be to integrate this modification in the build process.

There may be other techniques for changing MSI files programmatically, but I do it with the help of the WiRunSql.vbs script, which comes with the Windows Installer Software Development Kit (SDK). After installing the SDK, you can copy this script in the project folder and add a post-build step to update custom action types. To define a post-build step in the Visual Studio 2005 deployment (setup) project, do the following:

Make sure that the Properties window is open (select the Properties Window option from the View menu if needed).

Select the deployment project in Solution Explorer.

In the Deployment Project Properties window, click the PostBuildEvent option, and click the ellipses button that appears on the right side of the post-build event value.

In the Post-build Event Command Line dialog box, enter the following statements (make sure that each cscript statement remains on one line):

It would be better to use the bitwise OR operator to turn the msidbCustomActionTypeNoImpersonate bit on -- something like SET CustomAction.Type=CustomAction.Type|2048 -- but I could not make it work. Please be aware that if you use the hard-coded numbers shown in this example and Microsoft changes the generated values of the custom action types in future, you may need to update these queries.

Click OK to close the dialog box.

Once you build the project, make sure that the post-build step succeeds. If you do not see any errors, the modified MSI file should work on Vista.

There was bug in the original code sample that came with the article. Once I discovered it (two years ago) I immediately submitted an update, but it does not seem like the publisher replaced the original sample. If you want to use it, make sure that the ParseTime24 method of the DateTimeHelper class (in the My.Utilities project) contains the following statement (notice negation):

if (!IsTime24(time))
return false;

and not

if (IsTime24(time))
return false;

Also, add the following line (in bold) to the WeeklyThread's Start method:

// If the execution time is in the past, increment the day.
if (nextExecutionTime < StartTime)
nextExecutionTime = nextExecutionTime.AddDays(1);
// Set execution date depending on the day of the week.
NextExecutionTime = GetNextExecutionDay(nextExecutionTime, 0);