ISWIX, LLC View Christopher Painter's profile on LinkedIn profile for Christopher Painter at Stack Overflow, Q&A for professional and enthusiast programmers

Sunday, December 10, 2006

Managed Code CAN Access the MSIHANDLE

One of the ( many ) annoying things about InstallUtil is that it runs your managed code out of process where it can't access the MSIHANDLE. Once I was emailing with Bob Arnson about what would be involved to get managed code to be able to communicate with the MSI handle and he said:
You can host the CLR from native code, so theoretically, you
could start an appdomain from a type 1 CA and load and run an assembly from the
hosted appdomain. But that's only the start; you'd still have to marshal between
the two layers to give the managed code access to the
native-code MSI API. It wouldn't be a weekend job.
At the time I had not yet taken a C# class so I didn't really know what to do with this. Nearly a year has passed and I've actually managed to get enough C# reflection under my belt to think about what Bob had said. The result is a client server pattern similar to InstallUtil where the client is a Type 1 CA written in C++ and the server is a managed code class written in C#.

First let's look at the client side code:


#using <mscorlib.dll>
#using <system.dll>
#include "StdAfx.h"
using namespace System;
using namespace System::Windows::Forms;
using namespace System::Reflection;

extern "C" UINT __stdcall Test ( MSIHANDLE hMSI )
{
int _hMSI = hMSI;
array< int ^>^ args = { _hMSI };
Type^ typeCA = Assembly::LoadFile("D:\\ClassLibrary1.dll" )->GetType("ClassLibrary1.TestClass");
Object^ objectCA = System::Activator::CreateInstance(typeCA);
typeCA->InvokeMember("TestMethod",BindingFlags::InvokeMethod, Type::DefaultBinder,objectCA,args);
return ERROR_SUCCESS;
}

Basically we are using simple reflection here. We grab the MSI handle and shove it into an array. Next we load our assembly and get the type of our class. Finally we create an instance of the type and invoke our entry method passing it the argument array containing our msi handle.

Now let's look at the part of our C# code that will PInvoke to make our MSI API calls:


public class Session : System.MarshalByRefObject
{
private IntPtr
_handle;

[DllImport("msi.dll", CharSet =
CharSet.Unicode)]
static extern int MsiGetProperty(IntPtr handle, string
szName,
[Out] StringBuilder szValueBuf, ref int pchValueBuf);

[DllImport("msi.dll", CharSet = CharSet.Unicode)]
static extern int
MsiSetProperty(IntPtr handle, string szName, string szValue);

public string GetProperty(string propertyName)
{
StringBuilder
propertyValue = new StringBuilder(1024);
int size = 1024;
MsiGetProperty(
_handle, propertyName, propertyValue, ref size);
return
propertyValue.ToString();
}

public void SetProperty(string propertyName, string
propertyValue)
{
MsiSetProperty(_handle, propertyName,
propertyValue);
}

public void SetHandle( long handle )
{
_handle = new
IntPtr(handle);
}
}

Basically it's a class that wraps up MsiGetProperty() and MsiSetProperty() and exposes them as string GetProperty( string ) and void SetProperty( string, string ). Just instantiate the class, call the SetHandle() method passing it the MSI handle passed by the C++ code and your rolling. Let's see it in action:


using System;
using System.Runtime.InteropServices;
using
System.Text;
using System.Windows.Forms;

namespace
ClassLibrary1
{
public class TestClass
{
public void TestMethod(int
handle)
{
Session session = new Session();
session.SetHandle( handle
);
MessageBox.Show(session.GetProperty("ProductName"));
session.SetProperty("ProductName",
"Test");
}
}

To implement it, we merely wire the C++ code up as a Type 1 CA and store both DLL's in the binary table. The C++ could query the table and extract the record to a temp path for the reflection but I didn't need to code that since InstallShield has a built in pattern for extracting and cleaning up support files.

Speaking of InstallShield, unfortunatly the server side class would not work with CoCreateObjectDotNet(). They must be starting the CLR/AppDomain up out of process. InstallShield 12.5 maybe? Hint, hint.

Also this is obviously a prototype. I will be working on creating a complete implementation where the entire API is exposed and the client function is data driven so that it's reusable.

4 comments:

  1. Can we able to debug the C# code and C++ code (server and client) in VS.Net 2005 while installing?

    ReplyDelete
  2. Hi Chris,
    That is what I was exactly looking for when i stumbled upon your blog and found an answer to my solution.
    I did follow the steps as you metnioned... except that am using one MsiInterop class which I downloaded from CodeProject rather than creating my own. However, I always get an "Invalid Handle Error" when the .NET library tries to access the MSI database using the MSIHandle passed to it. I have verified that the value of Msi Handle in C# is same as in C++.
    Do you have any idea what could go wrong? would be glad if you could help me with this!

    ReplyDelete
  3. Hii,

    I have issue with this as following

    I have used VC++ for my setup. I had called the function during the setup, which is in my VC++ code.

    The function is as followed

    #pragma unmanaged
    #include "stdafx.h"
    #include "MSI_Logging.h"
    #include "stdafx.h"
    #using
    #include
    #include
    #include

    using namespace System::IO;
    using namespace System::Net;
    using namespace System::Net::NetworkInformation;
    using namespace System::Text;
    using namespace System;
    using namespace System::Collections;
    using namespace System::Collections::Specialized;
    using namespace System::Security::Cryptography;



    BOOL APIENTRY DllMain( HANDLE hModule,
    DWORD ul_reason_for_call,
    LPVOID lpReserved )
    {
    return TRUE;
    }



    UINT __stdcall SampleFunction2 ( MSIHANDLE hModule )
    {
    MessageBox(NULL, "Hello world", "CodeProject.com", MB_OK);
    array ^initVectorBytes = System::Text::Encoding::ASCII->GetBytes("@1B2c3D4e5F6g7H8");
    array ^saltValueBytes = System::Text::Encoding::ASCII->GetBytes("s@1tValue");
    array ^cipherTextBytes = Convert::FromBase64String("EMf/6yaltP7MXPoRo+XF6nwe3M0dzobeXY9UpSoSPTM=");


    PasswordDeriveBytes^ password = gcnew PasswordDeriveBytes ("BFEBFBFF0001067A", saltValueBytes,"MD5",9);
    array ^keyBytes = password->GetBytes(192 / 8);

    RijndaelManaged^ symmetricKey = gcnew RijndaelManaged;
    symmetricKey->Mode = CipherMode::CBC;

    ICryptoTransform^ decryptor;
    decryptor=symmetricKey->CreateDecryptor(keyBytes,initVectorBytes);

    MemoryStream^ memoryStream = gcnew MemoryStream(cipherTextBytes);
    CryptoStream^ cryptoStream = gcnew CryptoStream(memoryStream,decryptor,CryptoStreamMode::Read);

    array ^plainTextBytes;
    plainTextBytes = gcnew array(cipherTextBytes->Length);
    int decryptedByteCount;
    decryptedByteCount = cryptoStream->Read(plainTextBytes,0,plainTextBytes->Length);

    memoryStream->Close();
    cryptoStream->Close();

    String^ plainText;

    plainText = Encoding::UTF8->GetString(plainTextBytes,0,decryptedByteCount);
    return ERROR_SUCCESS;
    }

    This is is the function which I called during my setup. I refer following to call this function

    http://www.codeproject.com/KB/install/msicustomaction.aspx

    When I am run this setup I got following error

    "Attemp to use MSIL code from this assembly during native code initialization.
    This indicates a bug in your application. It is most likely the result of calling an MSIL-compiled (/clr) function from a native constructor or from DLLMain"

    What should I do?

    Thanks,

    Ankit

    ReplyDelete
  4. This thread is completely OBE by the launch of WiX's DTF solution. I would investigate that and forget about the mixed mode C++ stuff.

    ReplyDelete