65.9K
CodeProject is changing. Read more.
Home

WIX SSRS Custom Assembly Installer

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (4 votes)

Jul 9, 2009

CPOL

11 min read

viewsIcon

43231

downloadIcon

372

Windows Installer XML (WIX) project to install Custom Assemblies into Microsoft SQL Server Reporting Services (SSRS).

Introduction

This article will discuss the perils and pitfalls that I have encountered while building a Windows Installer XML (WIX) project to install Custom Assemblies into Microsoft SQL Server Reporting Services (SSRS). This WIX installer includes the ability to install, repair, remove, and upgrade multiple instances of your SSRS Custom Assemblies to instances of SSRS. The installer will detect all instances of SSRS found on the machine, and provide a dialog to allow the selection of an SSRS instance. This selection will cause the install to place the desired Custom Assemblies into the SSRS instance's application bin directory as well as modify the SSRS' security configuration file to allow proper execution permissions of the Custom Assemblies.

Background

Custom Assemblies are DLLs that are callable from SSRS reports. Since a machine may have multiple instances of SSRS, an installer is needed to target each instance of SSRS. In addition, it may be necessary to have two different versions of your Custom Assemblies in two different SSRS instances (Live / Test).

In this example, the Custom Assembly to be installed happens to be an Interop assembly (a .NET assembly that is a wrapper for an ActiveX / COM DLL). This adds some unique security concerns since Custom Assemblies need appropriate security permissions configurable in the rssrvpolicy.config in order to execute properly.

Reference Material

DummyInterop Project

This is a makefile project that creates a versioned .NET interop assembly that wraps an ActiveX / COM object that will be used by SSRS reports.

The .NET interop assembly DLL is created by using the Type Library Import (tlbimp) command on an Active X or COM DLL. This does not version the DLL, so the DLL needs to be disassembled into Intermediate Language and reassembled with a versioned resource file. For demo purposes, I've used the IEFrame COM / ActiveX control.

tlbimp ieframe.dll /out:%1\DummyInterop.dll
ildasm /nobar %1\DummyInterop.dll /out=%1\DummyInterop.il
rc.exe /r /fo %1\DummyInterop.res DummyInterop.rc
ilasm %1\DummyInterop.il /OUTPUT=%1\DummyInterop.dll /RESOURCE=%1\DummyInterop.res /DLL

CA_RSInstances Project

A C# DLL - added as a New Project (WIX / C# Custom Action Project). The project will create two DLLs. CA_RSInstances.dll which is a .NET assembly, and CA_RSInstances.CA.dll which is a C DLL that encompasses the .NET DLL, thus making it usable via WIX Custom Actions.

The Custom Action Project must reference Microsoft.Deployment.WindowsInstaller.

For each Custom Action to be called from the MSI, a declarative statement [CustomAction] must be present prior to the function declaration. Custom Action functions will be passed a Session object which houses the Installation Database information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Win32;

namespace CA_RSInstances
{
  public class CustomActions
  {
    [CustomAction]
    public static ActionResult CAFillRSInstanceListbox(Session session)
    {
      ......
    }

The CA_RSInstances DLL contains two Custom Actions:

  • CAFillRSInstanceListbox will populate a listbox of SSRS instances (including the version # if already installed), and a Custom Table in the MSI database to be used when an instance is selected for installation. The function enumerates the Registry's HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\RS to find all installed instances of SSRS. It also queries the Registry for Version and Install GUID to determine if this instance of SSRS already has a version of the Custom Assemblies installed.
  • Installation properties can be accessed or updated using the Session object:

    string ProductVersion=session["ProductVersion"];

    Installation tables (ListBox table and Custom Tables) can be accessed or updated using the View object:

    View listBoxView = session.Database.OpenView("select * from ListBox");
    Record newListBoxRecord = new Record(4);
    newListBoxRecord[1] = "RSINSTANCE";
    newListBoxRecord[2] = order++;
    newListBoxRecord[3] = Instance.Instance;
    newListBoxRecord[4] = Instance.Name;
    listBoxView.Modify(ViewModifyMode.InsertTemporary, newListBoxRecord);
     
    View availableRSView = session.Database.OpenView(
         "select * from AvailableRSInstances");
    Record newRSRecord = new Record(5);
    newRSRecord[1] = Instance.Instance;
    newRSRecord[2] = Instance.Name;
    newRSRecord[3] = Instance.FullPath;
    newRSRecord[4] = Instance.Version;
    newRSRecord[5] = Instance.InstalledGUID;
    availableRSView.Modify(ViewModifyMode.InsertTemporary, newRSRecord);
  • UpdatePropsWithSelectedRSInstance will query the Custom Table based upon the selected instance to retrieve the Path, Version, and Installed GUID. Based upon this information, various MSI properties need to be set in order to support installation of multiple instances / upgrade an existing installed instance / prevent downgrading to an older version.
  • View availableRSView = session.Database.OpenView(
      "Select * from AvailableRSInstances where Instance='" + RSInstance+"'");
    availableRSView.Execute();
    Record record = availableRSView.Fetch();
    if ((record[1].ToString()) == RSInstance)
    {
      RSInstance RS = new RSInstance();
      RS.Instance = RSInstance;
      RS.Name = (string)record[2];
      RS.FullPath = (string)record[3];
      RS.Version = (string)record[4];
      RS.InstalledGUID = (string)record[5];

    MSI properties to be assigned / cleared:

    • TRANSFORMS -- This informs the ExecuteSequence of the MSI to apply a Transform Instance specified.
    • INSTALLDIR -- Location to place the Custom Assemblies.
    • ProductName -- Property used to display the product once an instance has been selected.
    • Installed - Property used to determine if the Custom Assemblies have already been installed on this instance. If installed and not an Upgrade or a Downgrade, the MSI will enter a Maintenance dialog to repair / remove this instance.
    • MSINEWINSTANCE - Property used to force MSI to create a new Install / Upgrade instance.
    • NEWERPRODUCTFOUND - Propery used when the instance selected is already installed at a newer version. Setting this value will prevent downgrading.
    • UPGRADEFOUND - Property used when the instance selected is already installed, but at an older version that needs upgrade.
    • MIGRATE - Property works in conjunction with UPGRADEFOUND to migrate the current installed instance to the newer version.

InstallReportingServicesExtensions Project

The WIX project will utilize the CA_RSInstances Custom Actions and install the DummyInterop.dll into the selected SSRS instance location.

WIX utilizes XML tags to define all the features and functions within the MSI installer. I will be reviewing some of these tags and their meanings:

  • <? ?> is used to define a pre-compiler macro that can be later expanded using the $(var.xxx) syntax
  • <?define MainProductCode = "{d4e3d961-60db-11de-8a39-0800200c9a66}"?>
    <Product Id="$(var.MainProductCode)" ...>
  • <Product is the highest level tag that defines the entire contents of the MSI. It utilizes the following attributes:
    • Id - A unique GUID. When upgrading versions, the GUID should be changed to force a major upgrade.
    • Language - The decimal language ID (LCID) for the product.
    • Version - The product's version string (###.###.###.###) format.
    • UpgradeCode - A unique GUID. This GUID must remain the same for all versions. It is what is used when determining if another product has already been installed and has different versions.
  • <Package sets the attributes about the install package.
    • Id - A unique GUID. When upgrading versions, the GUID should be changed to force a major upgrade.
    • InstallerVersion - The minimum version of the Windows installer needed to install this package.
    • Compressed - Determines if the files in the package are compressed or not.
    • InstallScope - perMachine or perUser installation.
  • <Upgrade describes how the install will act when existing installations are found. It defines what properties will be set by the FindRelatedProducts install step.
  • <Upgrade Id="$(var.UpgradeCode_GUID)">
    
      <!-- Populate NEWERPRODUCTFOUND if there is an installed 
           package with the same upgrade code
           and version is > the version being installed -->
    
      <UpgradeVersion 
        Minimum="$(var.ProductVersion)" 
        IncludeMinimum="no" 
        OnlyDetect="yes" Language="1033" 
        Property="NEWERPRODUCTFOUND" />
    
      <!-- Populate UPGRADEFOUND if there is an installed 
           package with the same upgrade code
           and the version is between the earliest version defined
           and the version being installed -->
    
      <UpgradeVersion 
        Minimum="$(var.FirstProductVersion)" 
        IncludeMinimum="yes" 
        Maximum="$(var.ProductVersion)" 
        IncludeMaximum="no" 
        Language="1033" 
        Property="UPGRADEFOUND" />
    
    </Upgrade>
  • <Condition - Used to a enforce a condition to be met in order to install. Also defines conditions of UI controls and conditions on components to install.
  • <!-- Condition will fail with message if the user is not 
         a Privileged (admin) user-->
    <Condition Message="You need to be an administrator to install this product.">
      Privileged
    </Condition>
  • <CustomTable - Defines a custom table for use from a custom action.
  • <!-- Custom Table is used by the RSInstanceCustomAction to populate 
         information regarding each Reporting Services Instance on the machine, 
         and it's versions of installed extensions -->
    
    <CustomTable Id="AvailableRSInstances">
      <Column Id="Instance" Category="Text" PrimaryKey="yes" Type="string" />
      <Column Id="Name" Category="Text" PrimaryKey="no" Type="string" />
      <Column Id="Path" Category="Text" PrimaryKey="no" Type="string" />
      <Column Id="Version" Category="Text" Nullable="yes" 
        PrimaryKey="no" Type="string" />
      <Column Id="InstalledGUID" Category="Text" Nullable="yes" 
        PrimaryKey="no" Type="string" />
    </CustomTable>
    
    <EnsureTable Id="AvailableRSInstances" />
  • <Property - Defines a property that can be used in conditions / UI / custom actions. Can be referenced as [Property] in other locations in WIX files.
  • <!-- RSINSTANCE will be used to Identify the MSSQL.# that has been 
         chosen by the user-->
    <Property Id="RSINSTANCE"
      Value="NotSelected" />
  • <InstanceTransforms - Defines how an MSI database can be transformed by applying one or multiple transforms against the database. In our example, the transform, when applied, will replace the generic ProductCode GUID with a specific GUID per instance. In addition, the ProductName will be replaced with an instance specific name (this will what be displayed in the Add / Remove Programs list). Supporting multiple installation instances requires an instance to be defined for each of the allowed instances.
  • <!-- An instancetransform is necessary for each instance you 
         plan on supporting. The Instance Id will be used when 
         property TRANSFORMS=:XXXXXX -->
    <InstanceTransforms Property="RSINSTANCE">
      <Instance Id="MSSQL.1" ProductCode="$(var.Instance1ProductCode)" 
        ProductName="$(var.ApplicationSpecificProductName) MSSQL.1" />
      <Instance Id="MSSQL.2" ProductCode="$(var.Instance2ProductCode)" 
        ProductName="$(var.ApplicationSpecificProductName) MSSQL.2" />
    ...
      <Instance Id="MSSQL.10" ProductCode="$(var.Instance10ProductCode)" 
        ProductName="$(var.ApplicationSpecificProductName) MSSQL.10" />
    </InstanceTransforms>
  • <Directory - Specifies the directory layout and mappings between the source and target directories. Every WIX file will have a Directory Id="TARGETDIR" which is mandatory. Within this TARGETDIR directory, additional Directory elements will describe the directory location and the structure of components to install.
  • <Directory Id="TARGETDIR" Name="SourceDir">
    
      <!-- INSTALLDIR will be set by the UpdatePropsWithSelectedRSInstance 
           Once an instance is selected -->
      <Directory Id="INSTALLDIR" >
    ...
      </Directory>
    </Directory>
  • <Component - Items (files, Registry changes, XML changes, SQL changes) that you want the installer to place, change, or remove during an install or uninstall.
  • <Component Id="ProductComponent" Guid="$(var.ProductComponent_GUID)">
      <!-- File(s) to be place in INSTALLDIR-->
      <File Id="DummyInterop" 
            Name=" DummyInterop.dll" 
            src="$(var.SolutionDir)\DummyInterop\$(var.Configuration)\
    DummyInterop.dll"
            Vital="yes" 
            DiskId="1" />
    </Component>
  • <RegistryKey and RegistryValue - Used to describe Registry changes to be applied. Registry tags are sub items of a component. MSI recognizes non-files as shared items, and as such Registry items added during install will not get removed unless there is only one instance installed and uninstalled. In order to overcome this MSI limitation, each non-file component must have a unique component to handle non-file data. So in this example, for every instance we support, we must create a separate component group with the same Registry keys to apply / remove.
  • Note that there is a condition expressed in this component that will install this component if and only if the RSINSTANCE matches the instance we are defining the component for. This way, we can express multiple components, and based upon the RSINSTANCE property, only one will get installed / uninstalled.

    <Component Id="Registry_Instance1" Guid="$(var.Registry_Instance1_GUID)">
      <Condition> <![CDATA[RSINSTANCE = "MSSQL.1"]]> </Condition>
      <RegistryKey Root="HKLM" 
       Key="SOFTWARE\[Manufacturer]\[ApplicationSpecificProductName]\[RSINSTANCE]">
        <RegistryValue Id="RSEVersion.1" Action="write" Name="Version" 
           Value="[ProductVersion]" Type="string" KeyPath="yes" />
        <RegistryValue Id="RSEGUID.1" Action="write" Name="InstalledGUID" 
           Value="[ProductCode]" Type="string" />
      </RegistryKey>
    ...
    </Component>
  • <XmlConfig is a WIX utility extension tag that will allow for manipulation of XML files as part of a component install / uninstall. In order to utilize a WIX utility extension, the <WIX tag that describes the whole WIX file must include xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">.
  • In addition, you must add a project reference to WixUtilExtension.dll found in the Windows Installer XML bin directory created when WIX was installed.

    Note: In this example, we are placing an Interop DLL Custom Assembly which does not have .NET security built into it. Because no security exists, and we cannot be sure what security level is needed, the SSRS security file (rssrvpolicy.config) must be modified to enable FullTrust for anything that runs through the Report Expression Host Assembly. For a better understanding of SSRS Custom Assemblies and .NET security, please refer to the following: http://www.codeproject.com/KB/reporting-services/CustomAssemblies.aspx. This example sets the PermissionSetName to FullTrust when installed, and back to Execution when uninstalled.

    <util:XmlConfig Id="rssrvpolicy.config.1.install" 
        On="install" 
        File="[INSTALLDIR]\..\rssrvpolicy.config" 
        ElementPath="//configuration/mscorlib/security/policy/PolicyLevel/
    CodeGroup/CodeGroup[\[]@Name='Report_Expressions_Default_Permissions'[\]]"
        Action="create" 
        Node="value" 
        Name="PermissionSetName" 
        Value="FullTrust" />
    
    <util:XmlConfig Id="rssrvpolicy.config.1.uninstall" 
        On="uninstall" 
        File="[INSTALLDIR]\..\rssrvpolicy.config"
        ElementPath="//configuration/mscorlib/security/policy/PolicyLevel/
    CodeGroup/CodeGroup[\[]@Name='Report_Expressions_Default_Permissions'[\]]"
        Action="create" 
        Node="value" 
        Name="PermissionSetName" 
        Value="Execution" />;
  • <Binary is used to define a file that is needed in the package but is not installed.
  • <CustomAction is used to define the Custom Action entry points, and references the binary ID.
  • <!-- Define a binary file that will be used in the package and 
         referenced via custom actions 
         Must add the Project as a Reference in order to do 
         $(var preprocessing on project name varables -->
      <Binary Id="CA_RSInstances" 
        SourceFile="$(var.CA_RSInstances.TargetDir)\
    $(var.CA_RSInstances.TargetName).CA.dll" />
    
      <!-- Define Custom Action to Fill List of RSINSTANCE with 
           each Report Server Instance that is installed -->
      <CustomAction Id="GetRSInstances" BinaryKey="CA_RSInstances" 
         DllEntry="CAFillRSInstanceListbox" 
         Execute="immediate"  
         Return="check" />
    
    <!-- Define Custom Action to assign all necessary properties 
         based upon selected RSINSTANCE -->
      <CustomAction Id="UpdatePropsWithSelectedRSInstance" 
         BinaryKey="CA_RSInstances" 
         DllEntry="UpdatePropsWithSelectedRSInstance" 
         Execute="immediate" Return="check" />
  • <UIRef is used to pull in UI dialogs. They can be one of the standard UI configurations, or your own UI definition.
    • WixUI_Mondo offers the complete interface, welcome page, license agreement, setup type (typical, custom, and complete), allows for feature customization, browsing for the target directory, and offers disk costing. Maintenance mode dialogs are also included. You should use this flavor when some of your product's features shouldn't be installed by default (in other words, there is a significant and meaningful difference between typical and complete installs).
    • WixUI_FeatureTree is similar to the full set but it doesn't allow the user to choose between setup types. It always assumes Custom, and goes directly to the feature customization dialog after the user has accepted the license agreement.
    • WixUI_InstallDir allows the user to select a destination directory but without presenting the usual customized features page. After having selected the directory, the installation will proceed automatically.
    • WixUI_Minimal features a simplistic user interface with a single dialog combining the welcome and license agreement pages. After that, the installation goes on automatically without allowing the user to customize anything. Use it when your application has no optional features to install.

    In this example, I have built the following UI flow:

    • PrepareDlg
    • SelectRSInstance
    • WixUI_MaintenanceTypeDlg (if installed and not upgrading)
    • ProgressDlg
  • <dialog is used to define a UI dialog screen.
  • <!-- define UI screen to display the list of instances and allow selection -->
    <UI>
      <Dialog Id="SelectRSInstance" Width="370" Height="270" 
         Title="Reporting Services Extensions - Version [ProductVersion]">
    ...
    </Dialog>

    To view and manipulate a WIX dialog using a GUI tool, the SharpDevelop tool can be used. http://sourceforge.net/projects/sharpdevelop/.

  • <Control defines a GUI widget: Conditions may be placed on widgets; Publish is used to define actions or other dialogs to occur when the control is activated. In the below example, the Next Pushbutton will be disabled if RSINSTANCE is empty, and enabled if it is not empty. When the Next button is pressed, the Custom Action UpdatePropsWithSelectedRSInstance will be fired, this dialog will then be closed. Note that in the Publish definition, there is a condition check (in the below example, both Pubish definitions will always occur due to the 1 prior to the end of </Publish>. This can be any conditional expression to limit what events occur based upon settings.
  • <Control Id="Next" 
       Type="PushButton" 
       X="236" Y="243" Width="56" Height="17" 
       Default="yes" 
       Text="!(loc.WixUINext)">
       <Condition Action="disable">RSINSTANCE = ""</Condition>
       <Condition Action="enable"><![CDATA[RSINSTANCE <> ""]]></Condition>
       <Publish Event="DoAction" 
          Value="UpdatePropsWithSelectedRSInstance">1</Publish>
       <Publish Event="EndDialog" 
          Value="Return">1</Publish>
    </Control>
  • <InstallUISequence is used to define a dialog or Custom Action that fires in the UI sequence of events during the install. In the example below, the GetRSInstances Custom Action (which calls the DLL's CAFillRSInstanceListbox) is performed after PrepareDlg is displayed. The SelectRSInstance dialog is displayed after the GetRSInstances Custom Action is performed. MaintenanceTypeDlg is defined to show before the ProgressDlg based upon the defined conditions. A custom action SetRemove is used to assign the property REMOVE to the value ALL. This occurs if the user chooses to remove from the MaintenanceTypeDlg. The Custom Action PreventDowngrading will stop the install if there is a newer version already installed.
  • <InstallUISequence>
      <Custom Action="GetRSInstances" 
         After="PrepareDlg" Overridable="yes"></Custom>
      <Show Dialog="SelectRSInstance" 
         After="GetRSInstances">1</Show>
      <Show Dialog="MaintenanceTypeDlg" 
         Before="ProgressDlg">
    Installed AND NOT RESUME AND NOT Preselected AND NOT UPGRADEFOUND
      </Show>
      <Custom Action="SetRemove" 
         After="MaintenanceTypeDlg" 
         Overridable="yes">WixUI_InstallMode="Remove"</Custom>
      <Custom Action="PreventDowngrading" 
         Before="MaintenanceTypeDlg">NEWERPRODUCTFOUND</Custom>
    </InstallUISequence>
  • <InstallExecuteSequence is used to define Custom Actions that fire after the UI is finished and the install is starting to execute. The PreventDowngrading Custom Action will stop the install if there is a newer version already installed. If the <Upgrade tag is used, the RemoveExistingProducts tag must be specified in the ExecuteSequence. A Major Upgrade actually installs the new package, and uninstalls the originally installed package.
  • <InstallExecuteSequence>
      <Custom Action="PreventDowngrading" 
         After="FindRelatedProducts">NEWERPRODUCTFOUND</Custom>
      <RemoveExistingProducts After="InstallFinalize" />
    </InstallExecuteSequence>

Viewing the Completed MSI through ORCA

Once you have created your MSI package, you can view the MSI database through the ORCA tool. This can be helpful in determining if the UI and Execute sequence will launch as expected.

orca.jpg

Debugging the MSI

You can debug your MSI using the command line to execute your MSI and create a debug file using:

Msiexec -I NameofyourInstaller.msi -l*vc debug.log

This will create a debug file that includes interesting information like when a parameter value gets changed, and what parameters are passed to the execution sequence.

Sample output below:

Conclusion

Being new to MSI and WIX, building this project was a learning experience for me. I hope that this article can bridge the gap between the WIX manuals and a full fledged working installer. Many thanks to the writers of the articles listed in the Reference materials. Without their contributions, I would not have been able to make this work. With Microsoft Reporting Services being more widely accepted, I can foresee a great need to install custom assemblies on many reporting servers. This installer is generic enough that it should be a great plug-in to any application's installation suite.