【问题标题】:msiOpenDataBaseModes != 0 causes exceptionmsiOpenDataBaseModes != 0 导致异常
【发布时间】:2015-03-10 09:04:56
【问题描述】:

我完全不明白。

当我尝试以非只读模式打开 MSI 文件时,出现异常:

System.Runtime.InteropServices.COMException 未被用户代码处理 HelpLink=Msi.chm#9006 HResult=-2147467259 Message=OpenDatabase,DatabasePath,OpenMode Source=Msi API 错误 错误代码=-2147467259 堆栈跟踪:在 System.RuntimeType.ForwardCallToInvokeMember(字符串成员名称, BindingFlags 标志、对象目标、Int32[] aWrapperTypes、MessageData& msgData) 在 WindowsInstaller.Installer.OpenDatabase(String DatabasePath, Object OpenMode) 在基于 Web 的版本中 manager.AjaxFileHandler.updateMSIProperty(字符串 msiFile,字符串 msiProperty,字符串值)在 C:\Users\obfuscated\documents\visual studio 2010\Projects\基于网络的发布 manager\AjaxFileHandler.ashx.cs:line 28 at web based release manager.AjaxFileHandler.ProcessRequest(HttpContext context) 在 C:\Users\obfuscated\documents\visual studio 2010\Projects\web based 发布管理器\AjaxFileHandler.ashx.cs:第 143 行 System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() 在 System.Web.HttpApplication.ExecuteStep(IExecutionStep 步骤, Boolean & completedSynchronously)

可以使用下面的工作代码从 msi 读取属性,所以我知道文件路径是正确的:

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;
}

导致问题的代码:

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;
}

这两个函数是从同一段代码运行的:

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

我工作的公司正在迁移到以 msi 的形式发布软件。 没问题,除了公司由很多擅长他们所做的工程师组成,其中一部分是用于计算的编程工具。 他们不是计算机科学家,他们中的大多数人都不知道 OS2 和 Office 365 之间的区别...

到目前为止,大多数部门已经创建了一些发布系统,可以创建一个 msi,并安装产品,但是他们还没有真正掌握所有这些产品/包属性的作用。

我想我会帮助他们,在他们发布 msi 的我们的网络前端,通过替换 UpgradeCodes (guid) 并插入他们通常忘记的一些其他数据,例如制造商..证书等。

但我无法获取更新 MSI 的代码。

更新:

  • 对产品名称进行更多混淆,以保护工作。
  • 已尝试添加Marshal.FinalReleaseComObject(installer),但没有成功

【问题讨论】:

  • OpenDatabase: "如果方法失败,可以通过LastErrorRecord方法获取扩展错误信息。"
  • 好吧,我已经尝试过了,但是得到... 找不到成员。 (HRESULT 的异常:0x80020003 (DISP_E_MEMBERNOTFOUND))也在任何人想知道之前.. 是的,我有权写入文件..

标签: c# windows-installer


【解决方案1】:

除了 Chris 的建议之外,我会远离整个 COM 类型的激活,因为它完全没有必要。有一个非常好的 Win32 API,可以通过 p/invoke 使用。这是我曾经使用过的一个最小示例:

  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);
    }
}

请注意,这个惰性程序应该在每个句柄上包含对 MsiCloseHandle() 的调用,但不这样做,因为它无论如何都会在完成时发生。

【讨论】:

  • FWIW,WiX DTF 互操作库只是一组(经过精心设计和测试)抽象所有这些的类。在引擎盖下它使用 P/Invoke。我曾经按照您建议的方式进行操作(通常使用 Rich 的 MSIInterop.cs),但 DTF 方式远优于 IMO。
  • 仅供以后遇到此问题的任何人使用:如果您不想在同一个应用程序实例中重新打开数据库,则需要为此关闭句柄,您需要使用 MSIHANDLE 而不是 IntPtr , 或: -------------- [DllImport("msi", CharSet = CharSet.Auto)] public static extern int MsiCloseAllHandles(); -------------- MsiInvoke.MsiCloseAllHandles()
  • 不要忘记调用 MsiCommitDatabase 否则您将面临数据库损坏的风险。见备注部分:msdn.microsoft.com/en-us/library/aa370338(v=vs.85).aspx
  • 我添加了一条注释,说明这并没有按应有的方式关闭句柄,但那里已经有一个数据库提交调用。
【解决方案2】:

Windows Installer XML (WiX) 有一个称为部署工具基础 (DTF) 的功能,它有一个非常好的用于 MSI 的互操作程序集,称为 Microsoft.Deployment.WindowsInstaller.dll。该程序集有一个名为 Database 的类,该类具有如下构造函数:

public Database(
    string filePath,
    DatabaseOpenMode mode
)

一个简单的例子是:

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

还有一个实现 LINQ to MSI 模式的 QDatabase 类,可以轻松地将 Properties 表视为一个实体并进行相应的查询/更新。

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

我强烈建议您这样做,这样您就可以专注于您尝试编写的代码,而不是如何与 MSI 互操作。

【讨论】:

  • 我喜欢这个答案,它干净整洁,但是我给了 PhilDW “接受”,因为代码已经可以使用了,这是我在测试用例之前唯一缺少的东西是成功的。 - 我真的没有时间重做这个项目,但是下次我需要处理 MSI 时,我应该使用 DTF!
  • 重要的部分是避免使用 COM(两种解决方案都可以)并且记住始终关闭句柄(DTF 在这里表现出色,因为所有类都实现了 IDisposable)。当您只需要调用几个函数并且不想在您的解决方案中有另一个.DLL 时,interop.cs 方法我很好。但是对于任何超出 DTF 的东西来说,它都非常优越。
  • 是的,手柄关闭确实引起了意外,尽管它很容易修复(注意该解决方案的评论;)),我必须承认我在基于 COM 的解决方案中完全忘记了它,但现在它已在服务器端到位,并且更新了 get 函数以反映更改。
  • 至少,浏览一下 DTF(开放)源代码。它是由一位名叫 Jason 的 MSFT 开发人员编写的,他非常非常了解他的 MSI 知识。他知道所有这些陷阱,并且已经为我们解决了这些问题。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-23
  • 2011-06-08
  • 2017-02-16
相关资源
最近更新 更多