Tuesday, September 11, 2007

From real Steve to fake Bill

Summary: A few bits of entertainment and knowledge related to Apple and Microsoft, Steve Jobs and Bill Gates.

If you're following the buzz surrounding Steve Jobs and/or Apple (iPhone price cut, rebates, etc), you may enjoy the following articles:

The Puppet Master: Love Steve Jobs or hate him, just don't ignore him by Robert X. Cringely (interesting)
Dear early iPhone adopters: Yeah, we f****d you by "Fake Steve Jobs" (funny)
Larry's bold idea by "Fake Steve Jobs" (funny)

Not to be forgotten, here is your weekly dose of (well-deserved, yet good-natured) Microsoft mockery:

Channeling Microsoft Execs by John C. Dvorak (funny)

And a twist of iPod vs. Zune humor:

California by Joel Spolsky

Tuesday, September 4, 2007

Installing a Windows service on Vista

Summary (for developers): If your Windows service fails to install on Vista, these tips can help you correct the problem.

UPDATE: For information about implementing Windows services in Visual Studio 2008 (issues, solutions, and downloads), check my Implementing Windows Services in Visual Studio 2008 post.

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:

ActionTypeSourceTarget
_12345678_9ABC_DEF0_1234_56789ABCDEF1.uninstall1025InstallUtilManagedInstall
_FEDCBA98_7654_3210_FEDC_BA9876543210.install1025InstallUtilManagedInstall
_87654321_CBA9_0FED_4321_1FEDCBA98765.commit1537InstallUtilManagedInstall

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:
  1. Make sure that the Properties window is open (select the Properties Window option from the View menu if needed).
  2. Select the deployment project in Solution Explorer.
  3. 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.
  4. In the Post-build Event Command Line dialog box, enter the following statements (make sure that each cscript statement remains on one line):

    echo Updating the MSI file...
    cscript //nologo "$(ProjectDir)WiRunSql.vbs" "$(BuiltOuputPath)" "UPDATE CustomAction SET CustomAction.Type=3073 WHERE CustomAction.Type=1025 AND CustomAction.Source='InstallUtil' AND CustomAction.Target='ManagedInstall'"
    cscript //nologo "$(ProjectDir)WiRunSql.vbs" "$(BuiltOuputPath)" "UPDATE CustomAction SET CustomAction.Type=3585 WHERE CustomAction.Type=1537 AND CustomAction.Source='InstallUtil' AND CustomAction.Target='ManagedInstall'"

    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.
  5. 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.

See also:
Custom Actions under UAC in Vista
How to design msi packages for Windows Vista?
Windows Vista User Account Control Step by Step Guide


*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);


// Set default delay.
int delay = WakeUpInterval;
To get this and other updates to the code sample, download the modified project.