应用程序复原:解锁 Windows 安装程序的隐藏功能

 

迈克尔·桑福德
701 软件

2005 年 3 月

总结: Windows Installer 具有一些开发社区未注意到的功能。 这些功能允许应用程序在运行时修复自身,或者根据用户与应用程序的交互安装可选组件。 (10 个打印页)

下载 MSI 集成示例 Code.msi

目录

简介
通过 Shell 集成实现复原能力
Windows 安装程序 API 简介
关键应用程序 API
挑战 1:Self-Invoked 复原能力
挑战 #2:按需安装
结束语

简介

作为开发人员,我们确实倾向于考虑应用程序在理想环境、理想系统上运行,以及成功安装后使用应用程序的理想用户。 现实情况是,成功安装应用程序后,该用户的生存期才刚刚开始。 应用程序在保持稳定和正常运行方面可能面临的挑战很多,但大多数应用程序没有准备好处理操作环境中可能导致应用程序无法操作的更改。

Windows Installer 提供的复原功能在保持应用程序稳定方面取得了重大进展,但此功能基于用户在与 shell 交互时采取的某些操作,以提供 Windows Installer 通过这些“入口点”来检测应用程序配置问题并采取措施进行修复。

下面是 Windows Installer“入口点”的简短列表:

  • 快捷方式。 Windows Installer 引入了一种特殊类型的快捷方式,尽管对用户透明,但包含其他元数据,Windows Installer 通过 Shell 集成使用这些元数据在启动应用程序之前验证指定应用程序的安装状态。
  • 文件关联。 Windows Installer 提供了一种机制,用于截获对文档或文件的关联应用程序的调用,以便当用户使用 shell 打开文档或文件时,Windows Installer 可以在启动关联的应用程序之前验证该应用程序。
  • COM 广告。 Windows Installer 提供了一种连接到 COM 子系统的机制,这样,任何创建由 Windows Installer 安装的 COM 组件实例的应用程序 (并配置为使用此功能) ,在 Windows Installer 验证该组件的安装状态后,将收到该组件的实例。

在某些情况下,Windows Installer 的内置复原功能可能无法检测应用程序配置的所有问题,或者应用程序可能无法激活所需的入口点。 幸运的是,Windows Installer 团队中的聪明人理解了这一挑战,并通过丰富的 Windows Installer API 为我们提供了额外的复原功能。

通过 Shell 集成实现复原能力

在继续介绍 Windows Installer API 提供的高级复原功能之前,让我们看看使用 Windows Installer 部署应用时通常会免费获取的典型复原方案。

在此方案中,我们将部署一个简单的文本编辑应用程序,我们将调用 SimplePad。 若要创建安装,我们将使用 Microsoft 的开源 WiX 工具包 (,有关详细信息,请参阅 https://sourceforge.net/projects/wix/.) ,但你可以使用所选的任何工具完成相同的操作。

<Directory Id="TARGETDIR" Name="SourceDir">
<Component Id="SimplePad" Guid="BDDFA5DC-BD69-4232-998E-5167814C21B9" 
  KeyPath="no">
  <File Id="SimplePadConfig" Name="SP.cfg"
    src="$(var.SrcFilesPath)SimplePad.exe.config"
    LongName="SimplePad.exe.config" Vital="yes" KeyPath="no" DiskId="1" />
  <File Id="SimplePad" Name="Simple~1.exe"
    src="$(var.SrcFilesPath)SimplePad.EXE" LongName="SimplePad.exe" Vital="yes"
    KeyPath="yes" DiskId="1" >
  <Shortcut Id="SC1" Advertise="yes"  Directory="ProgramMenuFolder"
    Name="SimpPad" LongName="Run SimplePad"  />
  </File>
</Component>
<Directory Id="ProgramMenuFolder" Name="ProgMenu"></Directory>
</Directory>

如上面的 XML 片段所示,我们创建了一个非常简单的安装,其中包含一个文件 (SimplePad.exe) 和位于用户的“开始”菜单中的单个快捷方式。 请务必注意,在此示例中,我们创建的快捷方式是 Windows Installer 将用于检测应用程序状态并根据需要进行修复的入口点。

此时,我们可以生成安装程序、安装应用程序,并使用新创建的“开始”菜单快捷方式来运行它。 正如预期的那样,应用程序将完全按预期运行。 若要测试 Windows Installer 的内置复原功能,可以删除 SimplePad.exe 文件,并尝试从“开始”菜单快捷方式运行应用程序。 同样,如预期的那样,Windows Installer 检测到缺少 SimplePad.exe,并启动修复。 在修复操作期间,Windows Installer 从安装包的内部缓存副本中读取所需的配置信息,最后替换缺少的文件,如果源安装媒体不存在,则提示用户输入源安装媒体。 修复操作完成后,应用程序将正常启动。

图 1. 修复操作正在进行中

应用程序复原也由 Windows 安装程序通过一些其他机制提供,值得此处提及。 确保应用程序保持高可用性的第二种最常见方法是通过 Windows Installer 文件关联。 此机制的工作方式与 Windows Installer 快捷方式非常相同,但这种关联不是直接链接到应用程序的可执行文件,而是由已注册的文件类型进行关联。 如图 2 所示,Windows Installer 文件关联是使用标准文件关联使用的相同机制定义的,但有一个例外。 请注意,在图 2 中,一个额外的值列在典型的“shell\Open\command”注册表项下。 (也称为“命令”的附加值 ) 是 Windows Installer 在 Windows shell 中双击文件时,Windows Installer 将查找的位置。 这个看起来很神秘的字符串(有时称为“达尔文描述符”),实际上是特定产品、组件和功能的编码表示形式。 如果存在此额外值,Windows Installer 将解码数据,并使用它对该产品和组件执行检查。 如果发现组件缺失或损坏,Windows Installer 将启动修复以还原缺少的文件或数据,最后正常启动引用的应用程序,并向其传递相应的命令行选项。

图 2. 查看文件关联的“Darwin 描述符”

我们今天将讨论的最终复原机制通常称为 COM 广告。 在了解 COM 广告的机制之前,请务必了解其背后的用例。 假设你是提供基于 COM 的共享库的组件供应商,该库提供实时邮政费率。 由于此组件可由许多不同的产品使用,因此会将其安装到最终用户系统上的单个共享位置。 为了确保组件始终安装到同一位置,并确保组件保持高可用性,请在正确配置以利用 COM 广告的合并模块中将其交付给客户。 当然,由于解决方案作为单个 .dll 文件交付,没有用户界面,因此其他复原机制根本就不够用。 在这种情况下,我们可以依靠 COM Advertising 来确保组件在用户的系统上正确安装和注册。 当应用程序通过常规 COM 机制创建此组件的实例时,Windows Installer 会以与文件关联相同的方式“挂钩”到进程中。 请注意,在图 3 中,这次“Darwin 描述符”存储在组件的 COM 注册的 InprocServer32 注册表值中。 同样,Windows Installer 会解码并使用此信息,以确保组件正确安装和配置,根据需要执行任何修复,然后最终将组件的实例返回到调用应用程序。

需要指出的是,此独特功能与使用 组件的应用程序完全独立。 换句话说,即使使用组件的应用程序不是使用 Windows 安装程序安装的,组件使用的 COM 广告将继续正常运行,即使调用的应用程序只是 VBScript。

图 3. 查看 COM 服务器的“Darwin 描述符”

到目前为止,我们讨论和演示的所有内容都利用了 Windows Installer 的功能,而无需编写一行代码,但现在是时候继续实现更丰富、更可靠的了。

Windows 安装程序 API 简介

在前面的方案中,Windows Installer 的默认行为非常适合我们,但在现实世界中,我们的应用程序通常稍微复杂一些。 让我们扩展示例方案,以处理更具挑战性的方案。

应用程序通常由多个可执行文件组成。 一个示例可能是使用引导程序可执行文件为应用程序检查并安装更新的应用程序,如 Updater 应用程序块中所示。 在这种情况下,第一个可执行文件是在用户单击“开始”菜单上的快捷方式时调用的可执行文件。 反过来,它会启动包含应用程序main用户界面的第二个可执行文件。 根据安装的配置方式,Windows Installer 引擎很可能无法检测到main应用程序可执行文件的问题。 虽然一种选择可能是编写一堆在启动时运行的代码来检查运行时环境,但如果可执行文件本身缺失或损坏,并且无法轻松修复问题,则这根本不起作用。 一种更有效的解决方案是利用 Windows Installer 对部署包中已定义的应用程序配置的了解。

Windows Installer API 公开用于验证应用程序完整性的机制与用户与 shell 交互时所使用的相同。 通过从应用程序内部使用这些 API 调用,我们可以确保仍能实现相同的优势,而无需依赖前面讨论的 shell“入口点”。

下面是 Windows Installer 的 shell 集成复原功能未涵盖的方案列表:

  • 以 OS 开头的应用 (运行或运行一次的注册表项)
  • 系统服务
  • 计划任务
  • 其他应用执行的应用
  • 命令行应用

我相信,我们可以将更多方案添加到上面的列表中,但我认为你明白了。 在以下示例中,我将演示如何在不依赖于前面讨论的 shell 集成功能的情况下,获得 Windows Installer 复原能力的优势。 完成后,你将能够采用这些概念,并轻松地将它们应用于几乎任何涉及运行可执行代码的方案。

关键应用程序 API

在深入探讨一些示例方案之前,让我们看一下可从应用程序中使用的一些关键 Windows Installer API。 有关每个 API 使用情况的具体信息,请参阅平台 SDK 中的 Windows Installer Finction 参考

关键 Windows 安装程序函数 说明
MsiProvideComponent 检索组件的安装位置,根据需要进行安装或修复,以确保组件可用。
MsiQueryFeatureState 返回给定功能的安装状态。 例如,此函数会告知你是否安装了、未安装或播发了该功能。
MsiQueryProductState 返回产品的安装状态。 例如,此函数会告诉你产品是已安装、播发、为其他用户安装,还是根本不安装。
MsiConfigureProduct
MsiConfigureProductEx
这两个函数允许以编程方式安装或卸载应用程序。 MsiConfigureProductEx 通过允许你指定类似于在命令行上通常执行的操作的选项来提供更大的控制。
MsiConfigureFeature 使用此函数可以安装、卸载或播发应用程序的特定功能。
MsiGetUserInfo 此函数返回在产品的安装序列期间收集的用户、组织和产品序列号。
MsiGetComponentPath
MsiLocateComponent
这两个函数可帮助你确定组件文件或注册表项在目标系统上的物理位置。 MsiGetComponentPath 返回由特定产品安装的组件实例的路径,而 MsiLocateComponent 返回由 ANY 产品安装的组件的第一个实例。

挑战 1:Self-Invoked 复原能力

之前,我们讨论了一个非常基本的方案,在此方案中,我们可以实际从系统中删除应用程序的可执行文件,并使用快捷方式通过重新安装缺少的文件来使 Windows Installer 检测和修复问题。 虽然该方案适用于演示 Windows Installer 利用的 shell 集成,但为了更深入地了解这些概念,我们将介绍一个稍微复杂的方案。

在此方案中,应用程序由单个 .exe 文件和多个文本文件组成,这些文件为应用程序提供关键配置信息。

我们假设的软件公司的技术支持人员收到了大量支持请求,这些请求显示,由于用户通过双击 Windows 资源管理器中的可执行文件而不是使用安装创建的“开始”菜单快捷方式直接运行应用程序,Windows 安装程序无法解决应用程序配置问题。

在咨询团队的驻地部署专家后,我们的工程师团队决定,应用程序在启动时执行自己的复原检查以确保正确配置,从而大大受益。 为此,团队只需添加对 MsiProvideComponent API 的调用,以确保正确安装和配置应用程序安装包中定义的关键组件。

<DllImport("msi.dll")> _
Private Shared Function MsiProvideComponent(ByVal szProduct As String, ByVal _
 szFeature As String, ByVal szComponent As String, ByVal dwInstallMode As _
 MSI_REINSTALLMODE, ByVal lpPathBuf As System.Text.StringBuilder, ByRef _
 pcchPathBuf As IntPtr) As Integer
End Function

Public Shared Function ProvideComponent(ByVal productCode As String, ByVal _
 featureName As String, ByVal compID As String) As String
  Dim iRet As Integer
  Dim cbBuffer As Integer = 255
  Dim buffer1 As New System.text.StringBuilder(cbBuffer)
  Dim pSize As New IntPtr(cbBuffer)
  iRet = MsiProvideComponent(productCode, featureName, compID, _
   MSI_INSTALLMODE.INSTALLMODE_DEFAULT, buffer1, pSize)
  Return buffer1.ToString
End Function

为了更好地封装此代码,项目中添加了一个名为 WIHelper 的新类,以容纳 Windows Installer API 方法声明和包装器方法。 调用此代码只需向main窗体的 Load 事件处理程序添加几行即可。

Private CONST PRODUCTID As String = "PRODUCT_GUID_HERE"
Private CONST MAIN_FEATUREID As String = "DefaultFeatureKey"
Private CONST COMPID_1 As String = "COMP1_GUID_HERE"
Private CONST COMPID_2 As String = "COMP2_GUID_HERE"
Private Sub MainForm_Load() Handles MyBase.Load
  If WIHelper.IsProductInstalled(PRODUCTID) Then
    WIHelper.ProvideComponent(PRODUCTID, MAIN_FEATUREID, COMPID_1)
    WIHelper.ProvideComponent(PRODUCTID, MAIN_FEATUREID, COMPID_2)
  End If
End Sub
 

在上面的示例代码中,我们首先进行测试,以查看应用程序是否已实际通过安装包进行安装。 这是一个重要概念,因为我们希望确保即使在开发环境中进行调试,应用程序仍能正常运行。 为此,我们在帮助程序类中调用一个名为 IsProductInstalled 的方法。 反过来,此方法只需调用 MsiQueryProductState 来确定产品是否已安装在系统上。 如果调用 IsProductInstalled 表明产品已安装,则我们对帮助程序类中的 ProvideComponent 方法进行一系列调用。 同样,此方法是 MsiProvideComponent API 的简单包装器,它将返回指定组件的完整路径,并确保组件已正确安装并可供使用。 根据特定产品的需求,可以根据需要多次调用 ProvideComponent 方法,以确保应用程序完全可供用户使用。

挑战 #2:按需安装

我们假设的公司销售主管一直从客户那里听到很多反馈,他们希望看到一组标准模板与 SimplePad 一起交付。 虽然一些客户表达了对此功能的强烈愿望,但另一些客户对安装大多数用户可能不需要的无关数据表示担忧。

在无意中听到工程师们讨论如何处理这一新要求后,我们无畏的安装工程师跳了出来,指出 Windows Installer 只需少量的额外编码即可轻松处理此问题。

经过一些快速规划后,团队决定在应用程序的“文件”菜单中实现新的“模板”菜单项。 如果模板功能在本地安装在用户的系统上,则用户将看到一个浮出菜单,其中列出了每个可用模板,以及用于卸载模板功能的选项。 如果尚未安装模板功能,则模板弹出菜单将具有单个条目,使用户能够安装其他模板。

Private Sub mnuFile_Popup(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles mnuFile.Popup
  Dim newItem As MenuItem
  With mnuTemplates.MenuItems
    .Clear()
    If WIHelper.IsFeatureInstalled(PRODUCTID, TEMPLATES_FEATUREID) Then
      Dim dirInfo As New DirectoryInfo(Application.ExecutablePath)
      For Each dirFile As FileInfo In dirInfo.Parent.GetFiles("*.tpl")
        Dim mi As New MenuItem(Path.GetFileNameWithoutExtension(dirFile.Name))
        AddHandler mi.Click, AddressOf OpenTemplate
        .Add(mi)
      Next
      .Add("-")
      newItem = New MenuItem("Uninstall Templates")
      AddHandler newItem.Click, AddressOf UninstallTemplates
      .Add(newItem)
    Else
      newItem = New MenuItem("Install Templates")
      AddHandler newItem.Click, AddressOf InstallTemplates
      .Add(newItem)
    End If
  End With
End Sub

如你所看到的,我们首先检查以查看模板功能是否已安装。 如果是,则通过应用程序文件夹中扩展名为“tpl”的文件进行枚举,并将每个模板的名称添加到菜单中。 如果不是,只需为用户添加一个选项来安装模板。 在了解这一点之前,让我们先看看如何确定是否安装了模板功能。

<DllImport("msi.dll")> _
Private Shared Function MsiQueryFeatureState(ByVal szProduct As String, 
ByVal szFeature As String) As MSI_INSTALLSTATE
End Function    
Public Shared Function IsFeatureInstalled(ByVal pid As String, ByVal fid As String) As Boolean
  Return MsiQueryFeatureState(pid, fid) = MSI_INSTALLSTATE.INSTALLSTATE_LOCAL
End Function

在此简单函数中,我们只需调用 Windows Installer MsiQueryFeatureState 函数,并传入应用程序的 ProductCode 和我们正在查询的功能的名称。 如果 Windows Installer 返回INSTALLSTATE_LOCAL则返回 true,因为这意味着该功能在本地安装。

安装和卸载模板功能同样简单。

<DllImport("msi.dll")> _
Private Shared Function MsiConfigureFeature(ByVal szProduct As String, ByVal szFeature As String, ByVal eInstallState As MSI_INSTALLSTATE) As Integer
End Function

Public Shared Function InstallFeature(ByVal pid As String, ByVal fid As String)
  As Boolean
  Return MsiConfigureFeature(pid, fid, MSI_INSTALLSTATE.INSTALLSTATE_LOCAL) = ERROR_SUCCESS
End Function

Public Shared Function UninstallFeature(ByVal pid As String, ByVal fid As String) As Boolean
  Return MsiConfigureFeature(pid, fid, 
MSI_INSTALLSTATE.INSTALLSTATE_ABSENT) = ERROR_SUCCESS
End Function

当用户单击“安装模板”菜单项时,将调用 MsiConfigureFeature ,其中包含 ProductCode、要配置的功能的名称,以及指示我们希望在本地安装该功能的枚举值。 安装模板功能时,用户将看到 Windows Installer 进度对话框短暂显示。 当对话框消失时,模板将安装并可供使用。 当用户返回到“文件”菜单时,模板子菜单将使用上述模板的名称填充。

结束语

利用 Windows Installer 公开的“免费”功能和 API 为我们提供了一些很酷的功能,这些功能在降低支持成本、提高应用程序稳定性和增强用户体验方面大有裨义。 此处演示的示例本质上有些微不足道,但希望能成为实现自己独特解决方案的一个很好的起点。 我们了解了一些可用的 API,但我们当然没有涵盖所有这些 API。 花一些时间了解 Windows Installer API 的所有功能,我知道你会对利用 Windows Installer 中这些相对尚未开发的功能的轻松程度感到惊喜。

 

关于作者

Michael Sanford 是 701 Software (http://www.701software.com) 的总裁兼首席软件架构师。 在组建 701 之前,Michael 是 ActiveInstall Corporation 的总裁兼首席执行官,该公司被 Zero G Software 收购。 ActiveInstall 为其 Windows Installer 创作解决方案取得的恶名。 Michael 是 MICROSOFT 认证解决方案开发人员 (MCSD) 、Microsoft 认证系统工程师 (MCSE) 和 Windows Installer MVP。 你可以在 上阅读 Michael 的博客 http://msmvps.com/michael