From MSI to WiX, Part 22 - DLL Custom Actions - Introduction
Today I am starting a mini series on writing C++ custom actions.
Let's start with wizard-generated custom action project. Start Visual Studio and select "C++ Custom Action Project". Set "CAIntro" as the name of the project.
Before we will start discussing what is in the generated code, let's talk about what Windows Installer is expecting from dll in order to be a good behaving MSI custom action dll.
First of all, dll must provide the usual dll entry point - DllMain function. On process attach event we want to save HINSTANCE parameter in case later we will need to call some API function which requires it.
Second, we need to add custom action function. The signature of custom action entry point function is:
UINT __stdcall CustomActionEntryPoint(MSIHANDLE hInstall)
{
return ERROR_SUCCESS;
}
As you already know, valid return values from DLL custom action are:
- ERROR_FUNCTION_NOT_CALLED
- ERROR_SUCCESS
- ERROR_INSTALL_USER_EXIT
- ERROR_INSTALL_FAILURE
- ERROR_NO_MORE_ITEMS
Input parameter hInstall, passed by Windows Installer to custom action, is an installation handle. There are few functions we can call from custom action which require this parameter and we will discuss them later in this post and in subsequent posts as well.
Third, and last, we need to export custom action function in the definition file:
LIBRARY "CAIntro"
EXPORTS
CustomActionEntryPoint
Now, let's take a look at the generated code. Here is the content of CustomAction.cpp file:
#include "stdafx.h"
UINT __stdcall CustomAction1(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "CustomAction1");
ExitOnFailure(hr, "Failed to initialize");
WcaLog(LOGMSG_STANDARD, "Initialized.");
// TODO: Add your custom action code here.
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
// DllMain - Initialize and cleanup WiX custom action utils.
extern "C" BOOL WINAPI DllMain(
__in HINSTANCE hInst,
__in ULONG ulReason,
__in LPVOID
)
{
switch(ulReason)
{
case DLL_PROCESS_ATTACH:
WcaGlobalInitialize(hInst);
break;
case DLL_PROCESS_DETACH:
WcaGlobalFinalize();
break;
}
return TRUE;
}
WiX comes with its own API library which provides a set of convenient wrappers for Windows Installer API. We'll discuss them later in greater details. For now, let's make some changes to the project.
- Add new source file to the project. Right-click on "Source Files" and select Add->New Item..
- Click on "C++ File (.cpp)"
- In the Name text box type: GetHardwareProfile
- Click Add button
Now, cut the whole body of CustomAction1 function from CustomAction.cpp and paste it into GetHardwareProfile.cpp. Also, copy #include statement from CustomAction.cpp to GetHardwareProfile.cpp.
In GetHardwareProfile.cpp, rename CustomAction1 to GetHardwareProfile. Don't forget to change it in the WcaInitialize as well. Open CustomAction.def and change CustomAction1 to GetHardwareProfile.
For this sample we will use GetCurrentHwProfile windows API to set 3 properties: HWPROFILENAME, HWPROFILEGUID, and HWDOCKINFO. Here is the full source code for our custom action:
#include "stdafx.h"
UINT __stdcall GetHardwareProfile(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "GetHardwareProfile");
ExitOnFailure(hr, "Failed to initialize");
WcaLog(LOGMSG_STANDARD, "Initialized.");
// TODO: Add your custom action code here.
LPWSTR pwzPropertyValue = NULL;
HW_PROFILE_INFO hwProfInfo;
if (::GetCurrentHwProfile(&hwProfInfo))
{
hr = WcaSetProperty(L"HWPROFILENAME", hwProfInfo.szHwProfileName);
ExitOnFailure(hr, "Failed to set HWPROFILENAME property value");
hr = WcaSetProperty(L"HWPROFILEGUID", hwProfInfo.szHwProfileGuid);
ExitOnFailure(hr, "Failed to set HWPROFILEGUID property value");
hr = WcaSetIntProperty(L"HWDOCKINFO", hwProfInfo.dwDockInfo);
ExitOnFailure(hr, "Failed to set HWDOCKINFO property value");
}
else
{
hr = HRESULT_FROM_WIN32(::GetLastError());
ExitOnFailure(hr, "Failed to call GetCurrentHwProfile");
}
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
We will discuss all functions later, but for now I'll provide just short description of what is going on in here:
- WcaInitialize - initializes some WiX API internal variables. It alse caches hInstall to be used later in Windows Installer API calls.
- ExitOnFailure - If hr has an error code in it, logs message and passes control to LExit label.
- WcaLog - logs message to the log file.
- WcaSetProperty - set the string value to property.
- WcaSetIntValue - set the integer value to property.
- WcaFinalize - releases WiX API internal resources and returns final exit code.
And here is how we can call this custom action:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="https://schemas.microsoft.com/wix/2006/wi"
xmlns:util="https://schemas.microsoft.com/wix/UtilExtension">
<Product Id="95872e2d-8175-4661-a087-5aa8ca2db230"
Name="CAIntroInstall"
Language="1033"
Version="1.0.0.0"
Manufacturer="CAIntroInstall"
UpgradeCode="8124a77f-4991-47e6-aa63-557df0e51d26">
<Package InstallerVersion="200" Compressed="yes" />
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />
<Property Id="HWPROFILENAME" Secure="yes" />
<Property Id="HWPROFILEGUID" Secure="yes" />
<Property Id="HWDOCKINFO" Secure="yes" />
<Binary Id="CAIntro" SourceFile="..\bin\CAIntro.dll"/>
<CustomAction Id="HardwareProfile"
BinaryKey="CAIntro"
DllEntry="GetHardwareProfile"
Execute="immediate"
Return="check"
HideTarget="no" />
<InstallExecuteSequence>
<Custom Action="HardwareProfile" After="AppSearch" />
</InstallExecuteSequence>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLLOCATION" Name="CAIntroInstall">
<Component Id="ProductComponent" Guid="6335fc54-7f5a-46ae-9309-6b7ab214a00d">
<File Id="XmlFile"
Name="test.xml"
Source="test.xml"
KeyPath="yes" />
<util:XmlFile Id="SetHWPROFILENAME"
Action="setValue"
ElementPath="//config/parameter[\[]@name='ProfileName'[\]]/@value"
Value="[HWPROFILENAME]"
File="[#XmlFile]" />
<util:XmlFile Id="SetHWPROFILEGUID"
Action="setValue"
ElementPath="//config/parameter[\[]@name='ProfileGuid'[\]]/@value"
Value="[HWPROFILEGUID]"
File="[#XmlFile]" />
<util:XmlFile Id="SetHWDOCKINFO"
Action="setValue"
ElementPath="//config/parameter[\[]@name='DockInfo'[\]]/@value"
Value="[HWDOCKINFO]"
File="[#XmlFile]" />
</Component>
</Directory>
</Directory>
</Directory>
<Feature Id="ProductFeature" Title="CAIntroInstall" Level="1">
<ComponentRef Id="ProductComponent" />
</Feature>
</Product>
</Wix>
Functions which require an installation handle
Installation session information
- MsiGetLanguage
- MsiGetMode
- MsiSetInstallLevel
- MsiSetMode
- MsiGetSourcePath
- MsiGetTargetPath
- MsiSetTargetPath
- MsiVerifyDiskSpace
Features information
- MsiGetFeatureCost
- MsiGetFeatureState
- MsiGetFeatureValidStates
- MsiSetFeatureAttributes
- MsiSetFeatureState
Components information
Properties
Logging
Execute action
Evaluate condition
Database handle
Database functions
Obtaining a record containg all primary keys for a table
Get the status of database
Create view
View functions
Record functions
- MsiCreateRecord
- MsiRecordClearData
- MsiRecordDataSize
- MsiRecordGetFieldCount
- MsiRecordIsNull
- MsiRecordGetInteger
- MsiRecordGetString
- MsiRecordReadStream
- MsiRecordSetInteger
- MsiRecordSetString
- MsiRecordSetStream
Handle functions
In the next post we will take a closer look at our sample. We will discuss how to log a message to the log file and how to set property's value using Windows Installer API and how we did it using WiX API.
Source code for the sample is in attachment.
Comments
- Anonymous
January 07, 2010
The comment has been removed