Geeks With Blogs
Adventures of a Setup Developer my musings about setups and other things in my life
With the new .NET framework API providing such wonderful functionality, everybody is programming in C# or VB.NET except setup developers. There was quite some commotion in the WIX users list on writing managed custom actions. It was finally decided that it is generally harmful to write managed code custom actions as they would depend on .NET Framework and having dependencies for setup is plain bad design. But IMHO, we can use managed code custom actions if we are 100% sure that it would be present on the target platform or if it is a prerequisite and a part of your LaunchConditions.
 
Writing managed code custom actions can be tricky. The easiest way is to implement the System.Configuration.Install.Installer class. This Installer class has four methods which can be executed during the MSI thread. The Installer class was only meant for developers to do some dirty stuff. But it is far from perfect to be used as a part of the MSI thread. Some of my strong reasons for not using the Installer class are as below.
  • InstallUtilLib.DLL is not completely silent. It still pops up an ugly message box with it fails.
  • The methods do not have access to the MSI thread. So we would not be able to use properties or write to the MSI log file.
We are then left with only one choice. Writing DLLs with C++ which export functions which use managed code. One of the several places that this could be used is handling XML configuration data. Windows Installer supports writing properties to both INI and Registry files but we do not have such facility for XML files. I guess this should be on the TODO list for the next version of Windows Installer <smile/>. But until then we would have to do it ourselves. There is a neat task in NAnt named <xmlpoke>. This custom action is programmed in pretty much the same way without namespace support. Given the path to the XML file and the XPath expression, we would be able to replace the value with our own using the installer. And it should also support properties.
 
I start off by creating a class library project in VS.NET 2003 and start editing the ProjectName.CPP file. Note that you would have to create a Module Definition file to support exporting the DLL functions that you write as symbols. Now for the fun stuff.
 
Lets first write functions that can write to the log file.
 

UINT WriteToLog(MSIHANDLE hMSI, CHAR *strMessage)
{

MSIHANDLE hrec=MsiCreateRecord(1);
MsiRecordSetString(hrec,0, strMessage);
MsiProcessMessage(hMSI,INSTALLMESSAGE_INFO,hrec);
MsiCloseHandle(hrec);
return ERROR_SUCCESS;

}

The above function is a pretty simple function that just writes any string of characters to the log file. While this function is good enough, we will write another function that accepts the managed String type.

void WriteToLog1(MSIHANDLE hMSI, String* strMessage)
{

CHAR* strMessageArr={0};
strMessageArr=(char*)(void
*)Marshal::StringToHGlobalAnsi(strMessage);
WriteToLog(hMSI,strMessageArr);

}

This functions accepts the managed String* and Marshals it to a character array and then writes it to the log file. Now that we have the logging functions ready, lets concentrate on the actual juice. The idea here is that we use a deferred custom action to poke values into the XML file. The custom action expects data in a '|' delimited list in the following format.

[XMLFILEPATH]|[XPATHEXPRESSION]|[NEWVALUE]

A immediate type 51 custom action sets the CustomActionData property in the above format.

extern "C" __declspec(dllexport) UINT XmlPoke(MSIHANDLE hMSI)
{

TCHAR propval[261]={0};
DWORD len=261;
MsiGetProperty(hMSI,TEXT("CustomActionData"),propval,&len);
WriteToLog(hMSI,propval);
String *str=new
String(propval);
String *delims="|";
Char delim[]=delims->ToCharArray();
String* split[]=str->Split(delim);
FileInfo* fi=new
FileInfo(split[0]);
if(fi->Exists){

try{

XmlDocument* xdoc=new XmlDocument();
xdoc->Load(split[0]);
XmlNodeList * nl=xdoc->SelectNodes(split[1]);
IEnumerator * nodesenum=nl->GetEnumerator();
WriteToLog1(hMSI,split[1]);
while
(nodesenum->MoveNext())
{

XmlNode* xNode=__try_cast<XmlNode*>(nodesenum->Current);
String* oldValue1=xNode->InnerXml;
WriteToLog1(hMSI,String::Concat(oldValue1, new
String(" was the old value")));
xNode->InnerXml=split[2];
String* NewValue="New Value is ";
WriteToLog1(hMSI,String::Concat(NewValue,split[2]));

}

xdoc->Save(split[0]);

}

catch(Exception* exc){

WriteToLog1(hMSI,exc->Message);

}

}

else

{

WriteToLog(hMSI,"Error: The file does not exist");

}

return ERROR_SUCCESS;

}

The above function reads the CustomActionData property and splits it into an array. I assume that setup developers are good and they always give good data. So I have only gone for rudimentary error handling. The custom action never fails unless the error is catastrophic and is not handled by the try-catch block. It tries find if the XML file exists and if it does, it loads the XML file. I then use the SelectNodes() function to select the list of nodes as per the XPath expression. And finally I set the InnerXml value of the function to the new value. I also log these values as and when required. Thats it... as simple as it seems. 

Dont forget to have the following header files and namespaces on top of the file. You would also have to include reference to System.Xml.Dll.

#include "stdafx.h"
#include "windows.h" //Required by MSI.h
#include "XmlTasksManaged.h"
//The MSI Stuff
#include "Msi.h"

#include "MsiQuery.h"

//.NET Stuff goes here
using namespace System;
using namespace System::IO;
//For the FileInfo Object
using namespace System::Collections; //For the IEnumerator
using namespace System::Xml; //The XML Stuff
using namespace System::Runtime::InteropServices; //For the Marshal object

As mentioned earlier the Namespace support is still not included. It is not a very difficult functionality to add. We can pass it to the CustomActionData property. We can then cut and slice the CustomActionData property as we please. <smile/>

Posted on Monday, March 28, 2005 8:55 PM Setup | Back to top


Comments on this post: Writing managed code custom actions

# re: Writing managed code custom actions
Requesting Gravatar...
Cool. I was trying to do exactly this in C++ with MSXML, driven by tables, but when you already require .NET anyway this idea seems quite a bit better. It would be great if we could make something like this a SCA for WiX.
Left by Luke Stevens on Apr 03, 2005 9:28 AM

# re: Writing managed code custom actions
Requesting Gravatar...
Cool.. a very good idea, will deftly try this out
Left by Balaji on Apr 06, 2005 9:29 AM

# re: Writing managed code custom actions
Requesting Gravatar...
Very cool. However, because I am by no means a C++ guru, I could not get this to compile. I created a class library and placed the code you specified in my C++ file but it doesn't seem to work for me.

I get the following errors below. I tried on both VS.NET 2003 and VS.NET 2005 with the same results. Could you provide this sample code as a download?

Thanks

c:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include\ServProv.h(48): warning C4935: assembly access specifier modified from 'public'

c:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include\ServProv.h(93): error C2872: 'IServiceProvider' : ambiguous symbol
could be 'c:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include\ServProv.h(48) : System::IServiceProvider IServiceProvider'
or 'Stdafx.cpp(0) : System::IServiceProvider'
Left by Jake on Apr 18, 2005 10:35 PM

# re: Writing managed code custom actions
Requesting Gravatar...
Could you compare your way with this way for me?

http://weblogs.asp.net/cibrax/archive/2005/04/11/399839.aspx
Left by Owen Corpening on Apr 19, 2005 12:10 AM

# re: Writing managed code custom actions
Requesting Gravatar...
Jake,

I would not be able to upload the entire project on the site. However, If you send me an email using the 'Contact' link above, I would be more than happy to assist you.
Left by Vagmi Mudumbai on Apr 19, 2005 9:31 AM

# re: Writing managed code custom actions
Requesting Gravatar...
Thanks, Vagmi.

My email address is jake@codefreaks.net

Left by Jake on Apr 20, 2005 3:51 AM

# re: Writing managed code custom actions
Requesting Gravatar...
g'day (yes we really do say it in australia!)

the early portion of your posting suggests that this might be related to an issue i'm investigating. i'm trying to figure out how a .NET managed dll can be made to export a pure virtual interface and allow the exe to pass back another pure virtual interface for the dll to manipulate.

the dll loading is all done at runtime by the exe using LoadLibrary and GetProcAddress. i've been googling on and off for a couple of weeks now without finding anything clear enough for my VC6 oriented brain to comprehend. is this worthy of a blog entry?

rgds

dan
Left by dan.g on May 17, 2005 7:13 AM

# re: Writing managed code custom actions
Requesting Gravatar...
I'm no purist!
Apart from dependencies on .NET, what stopping a Custom Action DLL being written entirely in a .NET language (C#)? Can that be done with the aid of InstallUtilLib.DLL ?
For some of my applications that need to be delivered for .NET only, then my installer MUST fail if .NET v1.1 isn't there (on fail, then I would run the recently-released “Microsoft Component Installer Software Development Kit [Spring 2005]” found at www.microsoft.com/downloads/details.aspx?FamilyID=cd11be5a-c735-48d9-9cdd-a6d211c2e1c1&displaylang=en), which is a "common-dependencies" thing. Once .NET was in place, other dependencies and more elaborate conditions that are not currently checked by the Windows Installer SDK or WiX might be handled*.

A simple case is the requirement that an NTFS partition is available on the target system (simply knowing that the OS is post-Win2K is not sufficient). That's easy to check using VBScript, and also in .NET.

* - the "right" way is to let the MSI installer know about the situation, and I assume this should be done via an entry in the installer tables (since a "copy" of the MSI is always available, for reference). I'm still trying to understand all this, but - for the NTFS filesystem case - uninstalling is a "don't care" situation wrto NTFS, and there's nothing to restore or clear.

[What's so special about NTFS is that it can make use of Alternate Data Streams for each file, useful for ancillary information that is "tied" to the "main" file and so cannot easily be lost. Think of ADS as large buckets for custom properties.]
Left by Ian Thomas on Jun 01, 2005 9:07 PM

# re: Writing managed code custom actions
Requesting Gravatar...
I really find this article amazing and that is what I am trying to do. Could you send me the complete code? I will make a WiX sample and I will post it after it run flawlessly. My mail is omar.vera@jalasoft.com
Left by Omar Vera on Sep 17, 2005 3:37 AM

# re: Writing managed code custom actions
Requesting Gravatar...
Chris get's inspired by Vagmi and comes up with a simple pattern for managed code CA's.
Left by Christopher Painter on Mar 09, 2006 6:45 AM

# re: Writing managed code custom actions
Requesting Gravatar...
Chris discusses the completly refactored InstallScript implementation in InstallShield 12. Managed Code can now be invoked simply and reliably.
Left by Christopher Painter on Apr 29, 2006 8:07 AM

# re: Writing managed code custom actions
Requesting Gravatar...
I was getting the same error as Jake above in ServProv.h, adding the #include <windows.h> after the #include "stdafx.h" fixed the problem.
Left by Steve on Jul 18, 2006 1:46 PM

# re: Writing managed code custom actions
Requesting Gravatar...
I keep having the same problem in ServProv.h and adding #include <windows.h> didn't help either... any more ideas?!
Thanks, M
Left by Maria Koryakina on Aug 17, 2006 6:10 PM

# Deployment Tools Foundation - Managed CA's are Greenlighted
Requesting Gravatar...
First made available to the public in WiX weekly release 3.0.4116.0 (http://wix.sourceforge.net/releases/3.0.4116.0/ ), DTF provides a framework for easily and reliably writing managed code custom actions for the Windows Installer. In a nutshell, it provides a robust set of interop classes to simplify communicating with MSI and a hosting model to abstract the CLR code from the MSI process. At runtime, MSI thinks it’s calling a Win32 DLL in it’s own sandbox but in reality the CLR is being fired up out of process and communicated with through a named pipe. From your managed codes perspective, you are simply communicating with MSI through the interop classes with no need (generally) to be concerned with all of the nightmares of unmanaged code.
Left by Christopher Painter on May 20, 2008 8:43 PM

Your comment:
 (will show your gravatar)


Copyright © Vagmi Mudumbai | Powered by: GeeksWithBlogs.net