WIX SSRS Custom Assembly Installer






4.71/5 (4 votes)
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
- Creating and Deploying Reporting Services http://softwareexpertsolutions.blogspot.com/2007/06/creating-and-deploying-reporting.html
- Custom Assemblies in SQL Server 2005 Reporting Services http://www.codeproject.com/KB/reporting-services/CustomAssemblies.aspx
- Windows Installer XML (WIX) Toolset http://sourceforge.net/project/showfiles.php?group_id=105970&package_id=168888
- WIX Tutorial http://www.tramontana.co.hu/wix/
- WIX 3.0 Manual http://wix.sourceforge.net/manual-wix3/main.htm
- WIX Multiple Instance Transforms http://n2.nabble.com/Multiple-Instance-Transforms-Walkthrough,-Proposed-Simple-Addition-to-WiX-to-Make-Them-Easier-td708828.html
- Windows Installer SDK (including ORCA) http://www.microsoft.com/downloadS/details.aspx?FamilyID=e96f8abc-62c3-4cc3-93ad-bfc98e3ae4a3&displaylang=en
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 theExecuteSequence
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 withUPGRADEFOUND
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
orperUser
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 assumesCustom
, 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.
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.