Introduction
This article will step you through adding a custom action DLL to a Windows Installer setup. In my opinion there is a fairly large learning curve on creating MSI files, so this article will go step by step, on how to create a DLL, and how to add it to an MSI file. Be warned, this is my first article submission. I typed up these step by step instructions as I was creating the sample. So if you open the sample project, you will see the finished result. The same is true for the sample MSI file.
Step 1 - Creating the Project in VC++
Create a new project, and give it a name. In my sample, I used "MyCustomAction" as the project name. Select "Win32 Dynamic-Link Library", and click Next. Now select "A simple DLL project". This will create a basic DLL that does nothing. You can compile it without any errors, no big deal.
Step 2 - Adding required #includes and linking the .lib
Open up the stdafx.h file, and add the following:
#include <msi.h>
#include <msiquery.h>
#include <stdio.h>
Open up the Project | Settings (Alt + F7) and add "msi.lib" to the Link tab's Object/Library Modules section. You will want to do the same for the release and debug versions of your project. Do a compile to see if you need to add the directory of the SDK files to Visual Studio. You may need to point Visual Studio to the \Include\ and \Lib\ directories of the SDK.
Step 3 - Exporting functions
Now you will need to add a .def file to the project which tells the compiler which functions the DLL will be exporting (making available to the Windows Installer). Go to File | New, and select a new text file, and give it the name of your project, but use a .def extension. Now you need to add some stuff to that file. Rather than going into the details of it, and how a DLL works, I will just show you what I put in mine:
; MyCustomAction.def
;
; defines the exported functions which will be available to the MSI engine
;
LIBRARY "MyCustomAction"
DESCRIPTION 'Custom Action DLL created for CodeProject.com'
EXPORTS
SampleFunction
SampleFunction2
Step 4 - Creating the functions
Now that we have exported 2 functions, we obviously have to implement them. You will have to open up the .cpp file for your project. To be able to call a function from the MSI, it must be declared/implemented using the following syntax:
UINT __stdcall YourFunctionName ( MSIHANDLE hModule )
I've made the mistake of leaving out the __stdcall and pulled my hair out trying to figure out why the DLL compiles fine, but would never get executed during the install. After creating the functions, you will probably want to add something to them so you can tell that they were actually executed. In my sample, I added a MessageBox to both the functions. In my MessageBox call, I set the parent window to NULL. I would not recommend doing this, but for testing purposes, I can live with it. The problem with this is you the message box can end up hidden behind the MSI dialogs. If you needed to show a standard message box in a custom action, and keep it on top of the installation dialog (child window), you would need to implement it differently. If there is demand for it, I can type up another article that describes how to do that, using INSTALLMESSAGE_USER. At this point my .cpp file looks like:
#include "stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
return TRUE;
}
UINT __stdcall SampleFunction ( MSIHANDLE hModule )
{
MessageBox(NULL, "Hello world", "CodeProject.com", MB_OK);
return ERROR_SUCCESS;
}
UINT __stdcall SampleFunction2 ( MSIHANDLE hModule )
{
MessageBox(NULL, "Hello world", "CodeProject.com", MB_OK);
return ERROR_SUCCESS;
}
At this point you should be able to compile the program without any errors. You may have some un-referenced variable warnings, but that is it.
Step 5 - Editing the MSI
So hopefully you have an MSI already. If not, you can create a simple MSI using a number of tools such as InstallShield, Wise, or the FREE Visual Studio Installer from Microsoft. It doesn't matter which one you use to create the MSI, just make sure the MSI successfully installs and uninstalls. FYI, I created mine with InstallShield, then edited it so I wouldn't have to click through all the dialogs each time I test it. Mine does not install any files either, it just creates a regkey. For a good comparison between the editors available, visit www.installsite.org.
If you have not already installed Orca, you need to do so now. Orca is a MSI database editor that is included in the Platform SDK. I'm not sure where it is, but just search for Orca, and you should find Orca.msi somewhere in the SDK directory. Double click it to install. Now you can right on any MSI file and edit its contents. Also, another little tip, you can right click the MSI to uninstall it. This will save you time running to the add/remove applet after each test.
Binary Table
So, open your simple MSI up in Orca. I'm not going to explain what each table does, because that would take a week or more (thus the learning curve of Windows Installer that I mentioned before). So, on the left, click on the "Binary" table. This is where you can embed files into the MSI file. Add a new row to the binary table, and give it a Name. I used "CustomDLL". In the Data field, you will need to point it to the DLL you just compiled. Enter the path, or use the browse button to locate it.
CustomAction Table
Now navigate to the CustomAction table (click CustomAction on the left pane), and add a new row. It needs a name, which goes in the Action column. I used "CustomAction1". Set the Type = 1. The Source will be a foreign key back to the binary table. Since I called my entry in the binary table "CustomDLL", that is what I would enter for the Source. For the Target field, you will need to enter the name of the function in the DLL. So I entered "SampleFunction" in the Target column.
InstallUISequence Table
Those of you who are familiar with the Windows Installer will know that you can call the custom action practically anywhere. You can associated it with a button click, or as I am going to do, have it executed during the UI sequence. Navigate to the InstallUISequence table. Add a new row to this table, and for the Action field, this will be a foreign key to the CustomAction table, so enter the name of your CustomAction. In my case it was called "CustomAction1". For the Condition field, I am going to leave it blank. If you wanted this to only execute during an install, you can enter "Not Installed" as the condition. Now you need a sequence number. The sequence number determines what order these actions will occur. I suggest you find the Welcome Dialog (it may be called InstallWelcome, or practically anything). In my case its called InstallWelcome and it has a sequence of 650. So I want the Custom Action to take place before this, so I sorted by the Sequence to see what else came before it. I also wanted it to take place before the maintenance dialog, so I gave my new action a sequence number of 601.
Just so you are aware, adding our custom action to the InstallUISequence does not guarantee that it will be executed. If the user does a silent install, or uninstall, it will never get executed (nothing in the InstallUISequence gets executed during silent installs/uninstalls).
Step 7 - Test it!
Now save and close the MSI file. If all went as planned, when you double click the MSI, there should be a standard Windows Installer progress window, maybe a splash screen for your MSI, and finally a message box should pop up. Presto, you are inside the DLL! Programmatically, you can do anything you want inside the DLL. You can check to see if you application is running (via what ever method you want: FindWindow, CreateMutex, etc.). You can copy regkeys from your old product's registry key to the new version's. The possibilities are endless! I leave it up to you what you decide to do. Hopefully you did not get one of those cryptic Windows Installer error messages. Usually they have a 4 digit error code associated with them. If you got one, open and search through the Msi.chm to find a slightly longer explanation of the error.
Debugging
So the next question you will probably have is how to debug the DLL. Well, in my opinion, its kind of a pain. The process is described in the MSI.chm (Platform SDK). However I will provide you with an alternative to debugging.
Logging the MSI
Included in the source code is a .reg file. Double click this, and it will add the special registry key that will log every MSI install you ever run, even through the add/remove applet. The log files will show up in your %temp% folder, but the name of it is different each time, so sort by date and you should find it. This log file shows all the actions that the Windows Installer performs. You should be able to search for your custom action name, "CustomAction1" in my sample, to see where it got executed. It sure would be helpful to have some more logging show up there, rather than the time stamp of when the function call began and ended. That's where the next section comes into play.
Logging from the DLL
In my sample, I included 2 additional files. MSI_Logging.h and MSI_Logging.cpp contain a single function that will write a string of text to the log file. Just add these two files to your project, and add the #include "MSI_Logging.h" to your .cpp file, and you should be all set. In the example, you can see how I grabbed the Product Name from the MSI, and then formatted that into a friendly string to be written to the log file.
Conclusion
Hopefully this helps you get started in the world of MSI setups. As you can see, its just a DLL, so you ca do practically anything you want. As mentioned before, if there is demand for it, I can provide more examples relating to MSI setups, which is sort of my specialty.
History
28 Apr 2002 - Added an MsiMessageBox function for modal message boxes.
|
|
 |
 | Brilliant post. Followed the steps, worked first time ! neilsolent | 2:29 24 Sep '09 |
|
|
 |
 | msiexec.exe -Embedding custom action server switch..? Kabirdas Jaunjare | 22:36 25 Aug '09 |
|
 |
while installation of msi i was watching at process explorer ..i see multiple msiexec.exe are runing when i see porecces explorere properties i see like this
C:\WINDOWS\system32\MsiExec.exe -Embedding 59465E02FC8CDF15571B52DDDD811C42
i come to know this is for deffered custom action and i got the template like this.. msiexec.exe -Embedding <GUID> - this is the custom action server (indicated by the -Embedding switch)
here i have doubt what is <GUID> weatehr it stand for component ,Product or what its is...??
|
|
|
|
 |
 | MSI can't find the DLL? eranre | 4:15 25 Jun '09 |
|
 |
Hello,
I followed the steps to the letter, making a new DLL and pointing my existing msi (created with Windows Installer) to the location of the dll.
However, when I attempt to run the msi, I get a message that the installer is missing a DLL required for the installation, and it aborts.
Am I missing something? Is there a step to physically incorporate the dll into the msi (as a single packaged file)?
Thanks in advance,
Eran R.
|
|
|
|
 |
 | Calling EXE from MSI sumit_sri | 1:47 26 Mar '09 |
|
 |
How can I call a another exe (executable) from the MSI. My problem is to install another program from the MSI. Can you help me?
|
|
|
|
 |
 | how to Read the HKEY_CLASSES_ROOT\Installer\Products ProductName registry bhadridotnet | 5:45 12 Jan '09 |
|
 |
I want to read the HKEY_CLASSES_ROOT\Installer\Products ProductName registry to show the warning to the users If any older products has been installed. How can i implement those in the CustomActionDLL. Can you please send me the Code for Reading the Registry value and Show warning to the User.
Thanks Bhadri
|
|
|
|
 |
 | How to Query MSI lasida | 18:55 7 Dec '08 |
|
 |
Hi, I want to generate the list of files and folders need to be installed by my msi. How to query the .msi file for this information in C#. Please help. thanks in advance
|
|
|
|
 |
 | Dynamic Msi Creation? Firas Amm | 6:58 25 Jul '08 |
|
 |
Hi,
I would like to ask if I can use c# code to generate msi file?
Thanks, -Firas
|
|
|
|
 |
 | Unresolved external MsiSetProperty Ed.Poore | 7:56 2 Jul '08 |
|
 |
I've been porting a C# dll to C++/CLI (there are reasons) and including a CustomAction into the library. I'm now stuck on this error:
Error 3 error LNK2001: unresolved external symbol "extern "C" unsigned int __stdcall MsiSetPropertyA(unsigned long,char const *,char const *)" (?MsiSetPropertyA@@$$J212YGIKPBD0@Z) EntryPoint.obj Discovery.Licensing I've already included the necessary Platform SDK directories (C:\Program Files\Microsoft SDKs\Windows\v6.0A\) and the .lib and .h files both exist in the appropriate directories.
Any ideas on what's causing this? Am I missing an include of some form?
|
|
|
|
 |
|
|
 |
|
 |
Thanks but I'd fixed the error. All my paths were correct but it wasn't picking up the msi.lib in the Platform SDK\lib path, I had to explicitly add msi.lib to the command-line. After that it works fine.
|
|
|
|
 |
 | How do I act at Last Time? PeterPan1120 | 0:56 26 May '08 |
|
 |
I want to do action at last time that user click close button in normal flow. But I don't know this way. CloseForm is not able to delete and I am not able to find close button's action. Also I wonder that how do I know user don't click cancel button, close form is appeared when user click cancel button.
|
|
|
|
 |
 | Reading from a custom table Member 3537290 | 22:18 2 Apr '08 |
|
 |
Firstly let me thank you for you help on creating a custom DLL it has been invaluable.
I am attempting to read information from an added MSI table.
Something like this;
GetFromTable (TableName,FieldName) { Do some sort of SQL Statement Return value of FieldName }
Unit FunctionName (MSIHANDEL hmodule) { For Every Record in Custom MSI Table FieldValue1=GetFromTable(CustomTableName,FirstFieldName) FieldValue2=GetFromTable(CustomTableName,SecondFieldName) Do some stuff Next {
I was wondering if you have a sample of this sort of thing in action. Thankyou for your help.
|
|
|
|
 |
|
 |
Here is code to query the DB from a custom action. MsiRecordGetString or MsiRecordGetInteger gets the value - it gets what ever field you specify in the second parameter.
UINT result = 0; TCHAR szQuery[] = "SELECT DefaultDir FROM Directory"; PMSIHANDLE hDB = NULL; PMSIHANDLE hView = NULL; PMSIHANDLE hRecord = NULL; hDB = MsiGetActiveDatabase( hModule ); result = MsiDatabaseOpenView( hDB, szQuery, &hView ); result = MsiViewExecute( hView, hRecord ); while (MsiViewFetch( hView, &hRecord ) == ERROR_SUCCESS) { TCHAR szCurDir[MAX_PATH] = {0}; DWORD dwDirLen = MAX_PATH; if (MsiRecordGetString( hRecord, 1, szCurDir, &dwDirLen) != ERROR_SUCCESS ) break; // fail. break out of the while loop. // Do something. This sample code just pops up a message box. MsiMessageBox(hModule, szCurDir, MB_OK); }
|
|
|
|
 |
|
 |
Just what I wanted. Thank you.
|
|
|
|
 |
 | install multiple instances of the same msi Member 3317205 | 6:10 3 Jan '08 |
|
 |
Hello there,
First of all, this article is very useful, congratulation for creating it!
Workaround: I have a MSI file, which I want to install several instances of it. For that I am asking the user for a name for the instance. The user can add, remove or replace Instances at any moment.
Question: By changing the ProductCode, wasn't supposed the Windows Installer to install it as if it was program which wasn't installed ? Currently, after changing the ProductCode and executing the MSI file, the Windows Installer identifies the program as been already installed and asks if i want to repair or remove it.
thanks in advance, LA
|
|
|
|
 |
 | I got this far Peter Wone | 21:22 14 Sep '07 |
|
 |
This is a great article. I wish I'd found it before figuring out all this for myself.
I am nobody's C++ programmer. Most custom actions I can handle because they do nothing more complex than "if this value matches criteria and that field has a value return true" but recently I had to do a setupkit in which a custom action had to do things with sockets to find a remoting server, and then ask it for the Native Client connection string for a SQL Server, and then connect to the SQL Server and fetch a value from a table.
The socket stuff wasn't too bad but I had the devil's own time using SQLOLEDB in C++. I gave up and shelled a program written in C# but suspect it isn't all that hard to use SQLOLEDB if you know how. It might be a better idea to use ADO but that's another thing I don't know how to do in C++
Part of the trouble was that all the samples in the MDAC SDK are for VC6 and are not compatible with VC2005. Another part of the trouble was that all the samples were MFC applications, not minimalist DLLs, and a person of my limited VC experience couldn't tell where the MFC furniture ended and the sample proper began, or which bits of MFC (if any) were vital.
So I guess what I'm saying is that since you write well and you seem to know your way around VC, would you mind knocking up a sample of database connectivity in the context of a custom setup action DLL?
Also, there is a GPL wixwiki and although it has lain fallow for some time I am doing my best to jolly people into contributing. Would you mind a little wholesale piracy of this article? We already have a link to this article but I want to produce a single coherent ebook from the wiki, and therefore important material like this must be folded in.
PeterW -------------------- If you can spell and use correct grammar for your compiler, what makes you think I will tolerate less?
|
|
|
|
 |
 | How to set my own serial key [modified] Exelioindia | 21:56 9 Sep '07 |
|
 |
Hi,
I saw your article, I think you can able to help me in .NET deployment. I am deploying an project which was created using c# 2005. In this i want to ask the user to enter for an serial key. I tried using the Custom information serail key option, but i feel that it's not. I want to keep my own key as serial key and want to validate it. How to do this. plz giude me....
Thanks in advance
-- modified at 3:01 Monday 10th September, 2007
Know is Drop, Unknown is Ocean
|
|
|
|
 |
 | Installer fails while reading the function from DLL in Windows 2003 server naddynaresh | 3:00 26 Jul '07 |
|
 |
:(Hi, I created a Windows installer DLL using Visual Studio C++ and added a custom action in my installer for the dll. my .cpp file given below:
#pragma comment(lib, "msi.lib") #include "stdafx.h"
#ifdef _MANAGED #pragma managed(push, off) #endif
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { return TRUE; }
UINT __stdcall InstallDotNet(MSIHANDLE hInstall) { TCHAR szValue1[MAX_PATH] = {'0'}; DWORD pchSize = MAX_PATH; MsiGetProperty(hInstall, TEXT("NETFRAMEWORKV1_1"), szValue1, &pchSize); if(_wcsicmp(L"#1", szValue1)!=0) { UINT iResponse = MessageBox( GetForegroundWindow(), TEXT("This setup requires the .NET Framework version 1.1. Please install the .NET Framework and run this setup again. The .NET Framework 1.1 can be obtained from the http://www.microsoft.com/downloads/...57-034D1E7CF3A3. Click OK to download it or Cancel to exit Setup?"), TEXT("GPOAccelerator Tool 2.0"), MB_OKCANCEL | MB_ICONEXCLAMATION); if (iResponse == IDOK) { ShellExecute(NULL,_T("open"),_T("http://www.microsoft.com/downloads/details.aspx?familyid=262D25E3-F589-4842-8157-034D1E7CF3A3"),NULL,NULL,SW_SHOWNORMAL); return ERROR_INSTALL_USEREXIT; } else { return ERROR_INSTALL_USEREXIT; } } return ERROR_SUCCESS; }
UINT __stdcall InstallGPMC(MSIHANDLE hInstall) { TCHAR szValue2[MAX_PATH] = {'0'}; DWORD pchSize = MAX_PATH; MsiGetProperty(hInstall, TEXT("FILEEXISTS"), szValue2, &pchSize); if(_wcsicmp(L"C:\\WINDOWS\\system32\\gpmc.msc", szValue2) !=0) { UINT iReturn = MessageBox( GetForegroundWindow(), TEXT("This setup requires the Group Policy Management Console (GPMC). The GPMC can be obtained from the URL: http://www.microsoft.com/downloads/...72-dd3cbfc81887. Click OK to download it or Cancel to exit Setup?"), TEXT("GPOAccelerator Tool 2.0"), MB_OKCANCEL | MB_ICONEXCLAMATION); if (iReturn == IDOK) { ShellExecute(NULL,_T("open"),_T("http://www.microsoft.com/downloads/details.aspx?familyid=0a6d4c24-8cbd-4b35-9272-dd3cbfc81887"),NULL,NULL,SW_SHOWNORMAL); return ERROR_INSTALL_USEREXIT; } else { return ERROR_INSTALL_USEREXIT; } } return ERROR_SUCCESS; }
#ifdef _MANAGED #pragma managed(pop) #endif
=======================================================
my header files are: #include #include #include #include #include #include and
my .def file is
LIBRARY "installercheck" EXPORTS InstallDotNet InstallGPMC
This code is working fine in windows xp and vista machine. .msi file is working fine while run the installer. If i run the .msi file in Windows 2003 server machine, the installer fails without any fatal error with Return value as 3 in the installer log while reading the function from the DLL and prematurely ends the setup. I am not sure what is happening in windows server 2003 machine. Can any one guide me what would be the issue in .dll file while running from windows 2003 server?
Thanks, Naresh Krishna Kumar. K
|
|
|
|
 |
|
 |
Check your Windows Event Log, it probably says something like: Dependent Assembly Microsoft.VC80.CRT could not be found. I don't want to compile against the vc80 library. I just want the old C++ library no .net. I don't see a way in Visual Studio to compile against the old C++ library. I'm going to try converting all the code to C and compiling it as C code...then I know I won't get .net.
|
|
|
|
 |
|
 |
Go into the project properties: Configuration Properties | C/C++ | Code Generation. Select Runtime Library and set to whatever that isn't DLL. Then you won't need to ship any DLLs with it. Your dll will grow from about 5kb to about 60kb, but that's ok, since it goes into the MSI
|
|
|
|
 |
 | How to exit the msi and the Custom Action DLL on clicking Cancel button naddynaresh | 1:51 23 Jul '07 |
|
 |
Hi,
My requirement is to check whether .Net 1.1 is installed or not and if .Net 1.1 is not installed in the host machine, I will throw the error message asking the user to install the .Net 1.1 and if the user accepts the ok button, the installer will take the user to the download page of .Net 1.1 and similarly, I will be checking whether GPMC is installed or not and I will throw the error message if the GPMC is not installed and prompt the user with message box and if the user clicks ok button, the installer will take the user to the download page of the GPMC automatically. So I will be throwing the warning message from the CustomAction DLL using VC++. I have created the DLL, but the issue is after the user clicks the cancel button, the installer takes me to Welcome dialog screen instead of exiting the setup and also if the user clicks ok button, it takes the user to the download page of the .Net 1.1 and GPMC and at the same time, it will take to the welcome dialog screen. So I wanted to know how to exit the installer if the user clicks cancel button. I tried giving ExitProcess(), but still it takes to the welcome dialog screen of the installer. Thanks in advance.
Regards, Naresh Krishna Kumar. K
|
|
|
|
 |
 | How to get the MSIHANDLE Boyracer | 7:50 9 Jul '07 |
|
 |
I would like to call MsiSetMode but need the handle. How can the handle be passed from the installer to the custom action?
I tried to use the MsiOpenProduct. While the MessageBox pops up immediately, the MsiSetMode has no effect, assuming invalid_parameter.
thanks, markus ps: when using orca and simply using save as my .msi is much smaller in size and does not work any more
|
|
|
|
 |
|
 |
the MsiSetMode actually reports invalid handle. I have the CA added to the Install Sequence.
|
|
|
|
 |
 | Adding and Registering DLL to existing MSI MattFlynn | 1:42 3 May '07 |
|
 |
Hi,
I want to add my DLL to an existing DLL, so that it is available to an executable i have written. How do i do this?
Ideally i would like to add both the dll and exe to the msi, and these will be installed to a (ideally) user defined location (or set).
Many Thanks Matt.
|
|
|
|
 |
 | Custom action not working during uninstall arnkrishn | 21:46 29 Apr '07 |
|
 |
I have a .NET application. I needed to back up contents of the 'bin' folder present in the installation directory of the application. A DOS script does the job in this case. The intention is to call this DOS script before the product gets uninstalled. I've added a custom action for the same, which takes output from a project having windows installer class.
The installer class has the following code snippet -
Public Overrides Sub Uninstall(ByVal stateSaver As System.Collections.IDictionary) Try MyBase.Uninstall(stateSaver) Dim myProcess As New Process myProcess.StartInfo.CreateNoWindow = False myProcess.StartInfo.UseShellExecute = True
Dim f As New FileInfo("G:\Temp\test.bat") If (f.Exists) Then Console.WriteLine("file does exist") myProcess.Start("G:\Temp\test.bat") Do If myProcess.HasExited = True Then Exit Do Loop Else Console.WriteLine("File does not exist") End If
Catch ex As Exception MessageBox.Show(ex.Message) End Try
End Sub I feel the main MSI uninstall thread removes all the binaries from the 'bin' folder before my DOS script gets called. Is there someway to execute the DOS script before the binaries are removed and the registry entries get deleted ?
I guess there's some way to do this in C++ and not possible to do the same in VB/C#
Mgama your article on MSI Custom Action DLL is very good. I did learn things from there. In your article you speak of editing the Setup.msi using ORCA so as to make changes in the tables in the MSI database. Is there someway by which I can incorporate values to those tables using code ?
Thanks in advance. Arun
|
|
|
|
 |
|
|