DCSIMG
December 2011 - Posts - KMoraz's Sandbox

December 2011 - Posts

Installing a User Account Service Unattended (Part 2)

The previous post discussed about setting up the unattended service. Now that our shiny new service is ready for delivery, we need to package it inside a Windows Installer MSI.

There are two major alternatives to install a .NET service. The classic one being fully controlled by MSI in Win32 level; the other is overriding methods of System.Configuration.Install leaving it to the CLR via InstallUtil. Or you can combine both methods.

This post is about how to install a .NET service using WiX.

Packaging the service in WiX

Prerequisites: Visual Studio (2005/2008/2010 or higher), WiX toolset

  1. In Visual Studio, create a new Windows Installer XML setup project.
  2. We will need some UI to allow the user to enter the service account credentials. Add reference to WixUIExtension.dll

     

  3. Add the service component:
  4.     <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
            <Component Id="SampleService" Guid="D2C707DF-E365-4318-A466-863A7F4A4A74" SharedDllRefCount="yes">
                <File Id="SampleService.exe" Name="SampleService.exe" Source="$(var.BinFolder)\SampleService.exe" Vital="yes" />
                <File Id="SampleService.exe.config" Name="SampleService.exe.config" Source="$(var.BinFolder)\SampleService.exe.config" Vital="yes" />
            </Component>
        </ComponentGroup>
    
  5. Now we need to add properties to hold the username and password of the service account. Given the sensitivity of these properties, we'll mark them as secure and hidden:
  6.         <Property Id="SVCUSERNAME" Secure="yes" Hidden="yes" />
            <Property Id="SVCPASSWORD" Secure="yes" Hidden="yes" />  
    
  7. Optionally we can add a default value for the username via custom action:
  8.     <CustomAction Id="CA_Set_SVCUSERNAME" Property="SVCUSERNAME" Value="[ComputerName]\[USERNAME]" Execute="firstSequence" />
    
    This immediate custom action scheduled to run in the UI sequence:
        <InstallUISequence>
            <Custom Action="CA_Set_SVCUSERNAME" After="AppSearch">NOT Installed</Custom>
        </InstallUISequence>
    
  9. Include InstallUtilLib.dll in the binary table. While we could call InstallUtil.exe as well, this option guarantees silent execution.
    
  10.     <Binary Id="InstallUtil" SourceFile='$(env.windir)\Microsoft.NET\Framework\v4.0.30319\InstallUtilLib.dll' />
  11. The service is installed during the Execute phase so it's time add the custom actions that will run InstallUtil. Each action requires a type 51 set-a-property action counterpart to pass the CustomActionData to deferred execution.
  12.     <CustomAction Id='SampleService.Install.SetProperty' Property='SampleService.Install' Value='/installtype=notransaction /action=install /LogFile= /username=[SVCUSERNAME] /password=[SVCPASSWORD] /unattended "[#SampleService.exe]" "[#SampleService.exe.config]"' />
        <CustomAction Id='SampleService.Install' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred'/>
        <CustomAction Id='SampleService.Commit.SetProperty' Property='SampleService.Commit' Value='/installtype=notransaction /action=commit /LogFile= /username=[SVCUSERNAME] /password=[SVCPASSWORD] /unattended "[#SampleService.exe]" "[#SampleService.exe.config]"' />
        <CustomAction Id='SampleService.Commit' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='commit' />
        <CustomAction Id='SampleService.Rollback.SetProperty' Property='SampleService.Rollback' Value='/installtype=notransaction /action=rollback /LogFile= "[#SampleService.exe]"' />
        <CustomAction Id='SampleService.Rollback' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='rollback' />
        <CustomAction Id='SampleService.Uninstall.SetProperty' Property='SampleService.Uninstall' Value='/installtype=notransaction /action=uninstall /LogFile= "[#SampleService.exe]"' />
        <CustomAction Id='SampleService.Uninstall' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' Return='ignore' />
    

    Note: the service must have an app.config denoting its supported framework runtime version. Without it, the notorious error 1001 will follow once the service gets installed, failing the entire installation. The app.config defined as follows:

    <configuration>
      <startup>
        <supportedRuntime version="v4.0.30319"/>
      </startup>
    </configuration>
    

    See in the above example how the config path being passed as an argument ([#SampleService.exe.config]) after the service path.

  13. Now it's time to allow users to set the service account credentials through a form dialog.
  14. 8.1 The following is a simple implementation loosely based on the WixUI_InstallDir.

        <UI Id="WixUI_ServiceInstall">
          <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
          <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
          <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
     
          <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
          <Property Id="WixUI_Mode" Value="InstallDir" />
          <Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
     
          <DialogRef Id="BrowseDlg" />
          <DialogRef Id="DiskCostDlg" />
          <DialogRef Id="ErrorDlg" />
          <DialogRef Id="FatalError" />
          <DialogRef Id="FilesInUse" />
          <DialogRef Id="MsiRMFilesInUse" />
          <DialogRef Id="PrepareDlg" />
          <DialogRef Id="ProgressDlg" />
          <DialogRef Id="ResumeDlg" />
          <DialogRef Id="UserExit" />
     
          <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
          <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
     
          <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
     
          <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">NOT Installed</Publish>
          <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
     
          <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="AccountInformationDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
          <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
          <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
     
          <Publish Dialog="AccountInformationDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg">1</Publish>
          <Publish Dialog="AccountInformationDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
     
          <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
          <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
          <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
     
          <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
     
          <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
          <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
          <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
          
          <Property Id="USERVALIDATED" Value="0" />
          <Property Id="ARPNOMODIFY" Value="1" />
        </UI>
    

    8.2 The AccountInformationDlg dialog contains the SVCUSERNAME and SVCUSERNAME fields for the user to fill in:

        <UI>
          <Dialog Id="AccountInformationDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)">
            <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
            <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
            <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
              <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
            </Control>
            <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="Set user account information and click Next to continue" />
            <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="{\WixUI_Font_Title}User Account Information" />
            <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
            <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
            <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
            <Control Id="UserLabel" Type="Text" X="20" Y="60" Width="290" Height="13" Text="User Account (domain\user):" />
            <Control Id="UserEdit" Type="Edit" X="20" Y="74" Width="320" Height="18" Property="SVCUSERNAME" />
            <Control Id="PasswordLabel" Type="Text" X="20" Y="100" Width="290" Height="13" Text="Password:" />
            <Control Id="PasswordEdit" Type="Edit" Password="yes" X="20" Y="114" Width="320" Height="18" Property="SVCPASSWORD"/>
          </Dialog>
        </UI>
    

    8.3 Finally add the UI references in the <Product> element:

        <UIRef Id="WixUI_Common" />
        <UIRef Id="WixUI_ServiceInstall" />
    

    9. The project is ready for build. Update product properties and files and that's it.

     

Installing a User Account Service Unattended (Part 1)

In many productions environments, Windows Services required to run under a privileged domain account. NET services consume the Installer Class foundation to control its installation flow. In most cases, your .NET service ends up packaged in MSI installer which automates the actions InstallUtil does in command line.

But what if you want to install a user account service automatically without bothering the user (other than the service account user\password entries) with post installation actions? Commonly MSI authors will use the ServiceInstall Table which allows runtime entry of username (‘StartName’ column) and password among other properties. However, in case the target service is always expected to run under a domain account, since Installutil.exe (Installer Tool) 4.0 there is another solution.

Service Setup

  1. First, the service must be set with user account. Fortunately this is the default behavior of ServiceProcessInstaller.
    this.serviceProcessInstaller = new System.ServiceProcess.ServiceProcessInstaller();
    this.serviceProcessInstaller.Account = new System.ServiceProcess.ServiceAccount.User;
    this.serviceProcessInstaller.Password = null;
    this.serviceProcessInstaller.Username = null;
    
    	
    	
  2. Next, test that InstallUtil installs the service successfully when run from the Visual Studio command prompt:
  3. installutil.exe WindowsService1.exe

    During the install phase InstallUtil calls this 'Set Service Login' dialog:

    
    	
    	

    This is perfectly normal, since no username\password have been provided. Provide valid credentials and the service installed.

  4. Now we want to test unattended installation of the service.
    1. First we need to remove the service by adding the /u switch to the command: installutil.exe WindowsService1.exe /u

    2. And now repeat the test, this time without the prompt dialog: installutil.exe /username=[domain\user] /password=[password] /unattended WindowsService1.exe

    Note: the switches must precede the service path, otherwise be ignored.

    This feature of InstallUtil seems very little documented. In fact, I’ve noticed that by preceding a ‘/?’ before the service path, more information uncovered than found on MSDN.

    The command:
    installutil /? WindowsService1.exe
    The output – the interesting bit marked in yellow below: Microsoft (R) .NET Framework Installation utility Version 4.0.30319.1
    Copyright (c) Microsoft Corporation. All rights reserved.

    Usage: InstallUtil [/u | /uninstall] [option [...]] assembly [[option [...]] assembly] [...]]

    InstallUtil executes the installers in each given assembly.
    If the /u or /uninstall switch is specified, it uninstalls
    the assemblies, otherwise it installs them. Unlike other
    options, /u applies to all assemblies, regardless of where it
    appears on the command line.

    Installation is done in a transactioned way: If one of the
    assemblies fails to install, the installations of all other
    assemblies are rolled back. Uninstall is not transactioned.

    Options take the form /switch=[value]. Any option that occurs
    before the name of an assembly will apply to that assembly's
    installation. Options are cumulative but overridable - options
    specified for one assembly will apply to the next as well unless
    the option is specified with a new value. The default for all
    options is empty or false unless otherwise specified.

    Options recognized:

    Options for installing any assembly:
    /AssemblyName
     The assembly parameter will be interpreted as an assembly name (Name,
     Locale, PublicKeyToken, Version). The default is to interpret the
     assembly parameter as the filename of the assembly on disk.

    /LogFile=[filename]
     File to write progress to. If empty, do not write log. Default
     is .InstallLog

    /LogToConsole={true|false}
     If false, suppresses output to the console.

    /ShowCallStack
     If an exception occurs at any point during installation, the call
     stack will be printed to the log.

    /InstallStateDir=[directoryname]
     Directory in which the .InstallState file will be stored. Default
     is the directory of the assembly.


    Options for installing a Service Application:
    /username=name
        Sets the user account to run the service under. You must also
        specify the /password= option.

    /password=pwd
        Sets the password for the account to run the service under.

    The /username and /password options will be used only if the vendor of
    the service designated it as requiring a user account. If a service was
    so designated, and you do not use the /username and /password options,
    you will be prompted at install time for the account.
    /unattended
        Unattended install. Will not prompt for username or password.


    Individual installers used within an assembly may recognize other
    options. To learn about these options, run InstallUtil with the paths
    of the assemblies on the command line along with the /? or /help option.

    So by using these options we can pass username and password for unattended installation. This is great for taking control of the service installation flow from the MSI to the code.

    Next step is how to roll out this unattended feature within an MSI installer.

WordToTFS: Missing Fields

AIT's WordToTFS is a helpful tool, integrated as Word add-in for importing\exporting work items between TFS and Word.

On my first attempt to connect to TFS from Word, an error was raised:
Missing Fields:
"There are one or more mapping fields specified that are not available on the server. This might result in problems while publishing work items."

This message raised in case of mismatch between the TFS Work Item Template fields discovered upon connection to the add-in's fields of its current template.

 The solution is simply switching to the correct template:

  1. In WordToTFS tab, disconnect from the TFS.
  2. From the Template group, select the correct template. It must be the same template used on the target TFS project.
  3. Click Connect.
  4. Should the error reappears, it means the connected TFS project does not match the fields, which in case you need to use the Template Manager and make sure it shares the same version of the template.
Posted by kmoraz | with no comments