Henrik
Henrik

Reputation: 2229

msiOpenDataBaseModes != 0 causes exception

I don't understand this at all.

When I try to open an MSI file in anything else than read only mode, I get an exception:

System.Runtime.InteropServices.COMException was unhandled by user code HelpLink=Msi.chm#9006 HResult=-2147467259 Message=OpenDatabase,DatabasePath,OpenMode Source=Msi API Error ErrorCode=-2147467259 StackTrace: at System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Int32[] aWrapperTypes, MessageData& msgData) at WindowsInstaller.Installer.OpenDatabase(String DatabasePath, Object OpenMode) at web based release manager.AjaxFileHandler.updateMSIProperty(String msiFile, String msiProperty, String value) in C:\Users\obfuscated\documents\visual studio 2010\Projects\web based release manager\AjaxFileHandler.ashx.cs:line 28 at web based release manager.AjaxFileHandler.ProcessRequest(HttpContext context) in C:\Users\obfuscated\documents\visual studio 2010\Projects\web based release manager\AjaxFileHandler.ashx.cs:line 143 at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

I can read a property from the msi with the working code below, so I know the filepath is correct:

public static string GetMSIProperty(string msiFile, string msiProperty)
{
        string retVal = string.Empty;
        Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
        Object installerObj = Activator.CreateInstance(classType);
        WindowsInstaller.Installer installer = installerObj as WindowsInstaller.Installer;
        Database database = installer.OpenDatabase(msiFile, 0);

        string sql = String.Format("SELECT `Value` FROM `Property` WHERE `Property`='{0}'", msiProperty);

        View view = database.OpenView(sql);
        WindowsInstaller.Record record = null;
        view.Execute(record);
        record = view.Fetch();

        if (record != null)
        {
            retVal = record.get_StringData(1).ToString();
        }
        else
            retVal = "Property Not Found";
        Marshal.FinalReleaseComObject(installer);
        return retVal;
}

The code that causes the problems:

public void updateMSIProperty(string msiFile, string msiProperty, string value)
{
    Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
    Object installerObj = Activator.CreateInstance(classType);
    WindowsInstaller.Installer installer = installerObj as WindowsInstaller.Installer;

    var mode = MsiOpenDatabaseMode.msiOpenDatabaseModeDirect;
    Database database = installer.OpenDatabase(msiFile, mode);  //throws the exception!

    string sql = String.Format("UPDATE `Property` SET `Value`='{0}' WHERE `Property`='{1}'", value, msiProperty);

    View view = database.OpenView(sql);
    view.Execute();

    return;
}

And the two functions are run from the same piece of code as:

if(GetMSIProperty(path, "UpgradeCode") != theProductsUpgradeCode)
    updateMSIProperty(path, "UpgradeCode",theProductsUpgradeCode); 

The company I work for is migrating towards releasing software as msi's. No problem, except, the company consists of alot of engineers that are good at what they do, part of this is programming tools used for calculations. They are not computer scientists, and most of them wouldn't know the difference between OS2 and Office 365...

Up to this point most of the departments have created some release system, that can create an msi, and install the product, however they have not really grasped what all those product/package attributes do.

I thought I would help them, and at our webfrontend where they publish the msi's correct this, by replacing the UpgradeCodes (guid), and insert some of the other data they usually forget, such as manufacturer.. certificates etc..

But I can't get my code to update the MSIs.

Update:

Upvotes: 0

Views: 1395

Answers (2)

Christopher Painter
Christopher Painter

Reputation: 55601

Windows Installer XML (WiX) has a feature called Deployment Tools Foundation (DTF) that has a very nice interop assembly for MSI called Microsoft.Deployment.WindowsInstaller.dll. This assembly has a class called Database that has a constructor like:

public Database(
    string filePath,
    DatabaseOpenMode mode
)

A simple example is:

using Microsoft.Deployment.WindowsInstaller;    
using(Database database = new Database(@"C:\test.msi", DatabaseOpenMode.Direct))
{
  ...
}

There is also a QDatabase class that implements a LINQ to MSI pattern to make it easy to treat the Properties table as an entity and query / update accordingly.

using(var database = new QDatabase(@"C:\test.msi", DatabaseOpenMode.Direct))
// or in custom action
using(var qdatabase = session.Database.AsQueryable() )

I highly advise this so you can focus on the code you are trying to write and not how to interop with MSI.

Upvotes: 1

PhilDW
PhilDW

Reputation: 20790

As well as Chris's advice, I'd stay away from the entire COM-type activation because it's completely unnecessary. There is a perfectly good Win32 API that can be used via p/invoke. Here's a minimal example I used once:

  public class MsiInvoke
{

    [DllImport("msi", CharSet = CharSet.Auto)]
    public static extern int MsiOpenDatabase(string filename, int persist, out IntPtr dbhandle);
    public const int MSIDBOPEN_DIRECT = 2;

    [DllImport("msi", CharSet = CharSet.Auto)]
    public static extern int MsiCloseDatabase(string filename, int persist, out IntPtr dbhandle);

    [DllImport("msi", CharSet = CharSet.Auto)]
    public static extern int MsiDatabaseCommit(IntPtr hDb);

    [DllImport("msi", CharSet = CharSet.Auto)]
    public static extern int MsiViewClose(IntPtr hView);

    [DllImport("msi", CharSet = CharSet.Auto)]
    public static extern int MsiDatabaseOpenView(IntPtr hDb, string query, out IntPtr hView);

    [DllImport("msi", CharSet = CharSet.Auto)]
    public static extern int MsiViewExecute (IntPtr hView, IntPtr hRec); 
}
class Program
{
    static void Main(string[] args)
    {
        IntPtr hDb = IntPtr.Zero;
        int res = MsiInvoke.MsiOpenDatabase("setup.msi",MsiInvoke.MSIDBOPEN_DIRECT, out hDb);
        string qinsert = "UPDATE `Control` set `Control`.`Text`= 'Something' WHERE `Dialog_`='License_Dialog' AND `Control`='License'";
        IntPtr hView=IntPtr.Zero;
        res = MsiInvoke.MsiDatabaseOpenView(hDb, qinsert, out hView);
        res = MsiInvoke.MsiViewExecute(hView, IntPtr.Zero);
        res = MsiInvoke.MsiViewClose(hView);
        res = MsiInvoke.MsiDatabaseCommit(hDb);
    }
}

Note that this lazy program should include a call to MsiCloseHandle() on each handle, but doesn't because that happens when it finishes anyway.

Upvotes: 2

Related Questions