如何避免我的 WiX/MSI 部署解决方案中的常见设计缺陷?

How do I avoid common design flaws in my WiX / MSI deployment solution?

如何避免我的 WiX/MSI 部署解决方案中的常见设计缺陷?


部署是大多数开发的关键部分 - 部署失败意味着您的最终用户将永远无法评估您的产品。 这很容易成为软件开发中代价最高的错误。请给这个内容一个机会。我坚信,通过对应用程序设计进行微小更改,使部署更合理、更可靠,可以显着提高软件质量 - 这就是 "answer" 的全部内容 - 软件开发.


这是一个Q/A-style问题,其答案仅列出了一些不要在 MSI 文件中做的事情以避免最常见的设计缺陷。

WiX/MSI 部署Anti-Patterns

部署anti-patterns经常出现在WiX/MSI文件中。以下是一些最常见的 rough-draft。

在进入问题之前,另一方面here is a quick reminder of the things that have made MSI an overall success!(尽管有问题)。

This answer is a work in progress

What do you know I hit the max size for the answer. I guess that's a hint it's enough already :-). Some sections need clarification and improvement though.

如果您认识到其中的一些问题,您可能想继续阅读 - 这些都是众所周知的开发人员的痛恨和烦恼 Windows 安装程序/MSI:

  • 无法用最新的设置可靠地覆盖较低版本的文件。
  • 无法可靠地覆盖non-versioned 文件(例如对于 IIS)。
  • 在您尝试安装 MSI 升级后,文件神秘丢失
  • 数据在(主要)升级场景中被清除。例子包括:
    • 您的注册表存储的许可证密钥
    • 配置文件中的数据,例如config.xml、settings.ini等...
    • 您的 服务凭据 您不作为 LocalSystem 运行 的服务。
  • 数据未更新:
    • 设置文件在安装过程中无法可靠地更新为您要强制执行的新设置。
    • 您在更新按用户(或 HKCU)存储的数据文件中的设置时遇到问题。你为安装用户更新,你如何为其他用户更新?
  • 您发现 self-repair 意外地 为您的包裹启动。
  • 您的自定义操作 使设置炸弹出现神秘错误。
  • 这是一个大问题:您不必要地使用自定义操作来处理[=673=已经完全支持的事情] 安装程序本身。 这是一个庞大的部署anti-pattern,也是部署失败的主要原因。
    • 您通过自定义操作安装 Windows 服务。使用 built-in 构造在 MSI 本身中做得更好。
    • 您通过自定义操作将 .NET 程序集安装到 GAC。 Windows 安装程序本身完全支持,没有一行(有风险的)代码。
    • 您 运行 自定义 .NET 程序集安装程序 类。这些将用于开发和测试。作为部署的一部分,它们应该 永远不会 运行。相反,您的 MSI 应该使用 built-in 构造来部署和注册您的程序集。
    • 您 运行 先决条件设置和 运行time 安装程序通过您自己的 MSI 中的自定义操作。这应该完全不同地完成。请参阅第 6 节。
  • 部署共享运行时间文件
  • 时遇到问题
  • 静默安装 MSI 的安装状态似乎与运行交互式安装不同。

The sections below are in no particular order at all - as of now.

The sections are continually sought to be improved. Please add comments on what isn't clear or helpful.

Pending addition:

  • Difficult multi-instance installations
    • Relatively common requirement, especially for service installations
  • Uninstall is not working for MSI application - Error 1722
    • Service control: Failing to stop services before uninstalling
    • Uninstall CAs: Trying to run batch files / scripts that are no longer on disk during uninstall
    • Custom Actions: Erroneous conditioning so custom action runs unexpectedly. Often on uninstall or during major upgrades.

1。 Self-repair 问题

一个特别烦人的问题与经常为您安装的应用程序触发不需要的self-repair的构造有关。

  • 由于这个问题的 multi-faceted 性质,我创建了一个单独的答案来描述要避免的设计结构,以防止 self-repair 在没有警告的情况下发生攻击和您的申请意图:.

  • 有时 self-repair 用作使用应用程序设置填充 HKCU 或将文件放入每个用户的用户配置文件的方法。这通常可行,但我认为这不是应用程序设计和部署的最佳实践 - 请参阅下面第 9 节中的更多详细信息。

2。 共享、供应商或 Microsoft 运行时间文件的安装不正确

虽然这在上面的 link(self-repair 问题)中有广泛的解释,但这里也应该注意任何设置中最常见的错误之一是包含“本地共享 运行time 文件的副本”——如果它们是 COM 文件,有时也会在系统上全局注册。旧 VB6 应用程序的安装程序有时会为他们需要的常用控件执行此操作,从而破坏其他应用程序的系统。

  • 如果您需要特定版本的共享文件供 COM 使用,并且无法更新您的应用程序以使用正确安装的共享组件,那么您可以使用 registration-less 计算机。本质上是安装所需二进制文件的本地副本,并通过为二进制文件提供的清单文件强制加载共享文件。

  • 有关此主题的更多详细信息,请参阅上面第 1 项中的 self-repair 问题 link。

3。 (您自己的)共享文件和数据的不正确处理

如果你创造了一套MSI 文件来部署不同的产品,它们可能会在它们之间共享某些文件。如果您从多个 MSI 文件定位相同的文件位置(绝对路径)——每个文件使用不同的组件 GUID,那么每个安装程序都会将该文件视为“拥有它”——在卸载时愉快地卸载它,或将其重新放置到位通过 self-repair.

  • 正确的解决方案是认识到对于您定位的每个绝对路径,都必须有一个组件 GUID。绝对路径由组件 GUID 引用计数 - 它必须在您的所有设置之间共享才能正常工作。

  • 要在所有设置中使用相同的组件 GUID,您应该创建一个 merge-module 以包含在每个设置中,或者在 WiX 中使用高级构造,例如“包含文件”-对其中包含的组件使用硬编码 GUID。

  • 如果有问题的文件是数据文件,更新后永远不能卸载或替换,您还应该考虑将其安装为“永久组件”,以便在重大升级期间不会被卸载或手动 运行 卸载。

4。 组件创建错误 - 未遵循最佳实践

没有遵循组件创建的最佳实践。 MSI 组件是文件和注册表设置的基本安装单元。

  • 有关于如何“组件化”应用程序文件的最佳实践规则。违反这些规则可能会导致补丁和升级出现问题,并出现神秘症状,例如升级后丢失文件和设置,或者补丁因无意义的错误而崩溃。

  • 为了解决这个问题,过于简单化的做法是您应该为每个组件使用一个文件,除非您的设置中的文件数量确实非常庞大。 This avoids all kinds of problems(阅读 link 以获得对组件 ref-counting 的更详尽解释)。

5。 与用户数据被覆盖或重置相关的升级问题

这不亚于极其普遍。我已经回答了几个关于这个主题的 Whosebug 问题,并且它不断出现。

  • 请阅读名为“过度使用 per-user 文件和注册表部署”的部分,了解如何最大限度地减少对 Windows 一般 user-data 部署的安装程序。如果你问我这是解决这些持续存在的“数据还原”问题的真正答案。

  • 由于 MSI 中的升级很复杂,因此许多人对 主要升级(最简单的升级形式)进行了标准化。主要升级本质上是卸载并重新安装同一产品(不同版本)。

  • 有几种方法可以配置这样一个重大升级,但是如果在安装新版本之前完全卸载旧版本,则可以卸载已修改的用户数据文件自安装以来。 MSI 不会检查数据文件自安装后是否已被修改,并且会毫不犹豫地愉快地卸载它们,除非您已将托管组件标记为“permanent”(它永远不会被卸载)或 为托管组件设置一个空白组件GUID(一个特殊的功能来安装文件然后完全忽略它)。

  • 需要注意的一个特殊情况是,即使您通过使用合并模块或 WiX 包含文件正确共享此类文件(以保持安装组件 GUID stable) - 如果当时只有一个产品引用了它(引用计数为 1),它可能仍会通过重大升级被卸载和重新安装。

  • 主要升级完成后,看起来数据文件已被覆盖或还原,但实际上修改后的数据文件只是简单地卸载,然后重新安装到它们的“新版本”(很快就会更新一些潜在的修复程序。

  • 在我看来你应该只安装安装后使用的数据文件read-only。如果要写入文件,我认为它们应该由应用程序本身生成,并存储在 user-profile 中。这是一个示例,说明如何更改应用程序设计以使部署更加可靠。我认为的“真正的解决方案”。

  • 如果您确实安装了带有组件的 read/write 数据文件,请将其设置为永久(或使用空白 GUID)。文件覆盖规则将确保磁盘上的文件在安装过程中不被覆盖(除非你做了一些愚蠢的事情,例如将 REINSTALLMODE 设置为 amus 以强制覆盖所有文件 - 这绝不应该被允许。它可以降级由 merge mod 安装的共享文件文件 - old-style DLL 地狱)。如果您确实想擦除文件并覆盖它,也可以使用各种方法,其中最好的方法可能是使用配套文件。 (更多细节稍后补充)。

  • Wix: Windows Service sometimes uninstalled when upgrading

6. 错误或不必要地使用自定义操作

(过度)使用 MSI 文件的自定义操作是一个很大的话题,这部分太大了,被分成了一个单独的答案: .

由于 built-in MSI 支持实现相同的效果,或者 ready-made 免费框架(如 WiX)或商业工具(如 Advanced Installer)中的解决方案可用,因此通常不需要自定义操作或 Installshield。

自定义操作本质上很容易出错,是导致部署失败和错误的主要原因。详情请阅读以上link。 数千人、数万人,甚至数百万人测试了这些 built-in 结构。你到底为什么要自己做?

一些“besserwissing”(我自己应该遵循的建议):关注让您的产品与众不同的地方 - 它的新之处,并消除所有其他错误来源。好的部署不会成就你的产品,但糟糕的部署会毁了它。

7. 未能正确合并 INI 文件

可以通过文件 table 安装 INI 文件 - 就像安装任何其他文件一样。如果目标位置存在现有 INI 文件,则不允许合并。

  • 如果您将 INI 条目导入适当的 MSI tables,您可以使用“合并”与现有值更新现有的 INI 文件,而不仅仅是覆盖文件“清除”现有条目,或者根本不更新文件。

  • “INI 合并”是“auto-magic”,它允许适当的回滚支持和“pin-pointed”更新任何现有 INI 文件中的值。如果安装程序中止,INI 文件将正确恢复到其初始状态。

  • 这是一项出色的功能,对我见过的几乎所有 INI 文件都非常有用。但是,我确实看到过一些 INI 文件具有 non-standard 格式的情况。有时他们有你想要安装的大型评论部分(开发人员工具)或 MSI 合并不支持的奇怪格式(逗号分隔的三重文件和类似的东西)。在这些情况下,您必须将其安装为文件而不是“更改事务”以保留唯一格式的 INI 文件。

  • 如果您正在开发和使用 non-standard INI 文件,请考虑为该文件提供不同于 *.INI 的扩展名,以表明其独特性和特殊处理的需要。它实际上不再是 INI 文件(key-value 格式)。反之亦然:你有一个独特的扩展名,如果文件内容是 key-value 对,你可以将它更改为 INI 以将其作为适当的 INI 文件处理。

8。 错误地为 COM 文件使用 self-registration

或通过注册表安装注册 table。使用适当的 COM 广告 tables。原因有很多,如下所述:Self-registration considered harmful.

  • 我曾见过 self-registration 在相关系统上执行实际 COM 注册以外的其他操作。这通常是相关开发人员的可怕设计,但我知道有些情况下人们选择使用 self-register 而不是 re-implement 在 self-registration 期间完成的操作作为适当的自定义操作。

  • 允许发表个人意见:当我看到网络设置受到self-registration的影响时,我立即希望该软件完全被拒绝使用。这就是在self-registration这样的标准化操作中做如此“hacky”的事情是多么严重。理智的问题是“考虑到那个狡猾的 COM 注册,他们还要做什么”。依赖 non-standard 骇人听闻的东西并不能建立信心。

9. 过度使用 per-user 文件和注册表部署

升级:与此主题相关的新答案:Create folder and file on Current user profile, from Admin Profile

这部分内容太大,被拆分成一个单独的答案:

本质上 user-profile 在 HKCU 中部署文件或设置是可以容忍的,但这可能不是最好的设计,并且确保所有设置和文件都进入每个 user-profile 和盒子上的用户注册表。在上面的 linked 回答中讨论了导致的部署问题和一些建议的解决方案。

基本上可以使用 MSI self-repair、Microsoft Active Setup 或通过对应用程序或解决方案的逻辑设计更改来支持用户部署 i问题(首选选项 - 有关详细信息,请参阅 linked 答案)。一般来说,部署不应干扰用户数据和设置,因为它确实是用户数据,不应部署,而是由应用程序在 运行 时间生成。

10。 静默安装无法完成或未完成

built-in Windows 安装程序的一个特点是任何 MSI 文件都可以在静默模式下安装。这是旨在帮助企业部署的技术的核心功能 - 通常总是 运行 处于静默模式。 确保您的 MSI 能够在静默安装后完成并成功运行,这一点非常重要。根据我的经验,自定义操作通常会导致静默安装出现问题。

  • 切勿在 InstallUISequence(从您的设置对话框)中对计算机进行更改。上面已经描述了这个问题。交互式 GUI 中使用的自定义操作是即时模式(普通用户无需提升)并且应该只收集和验证用户输入 (read-only)。对计算机所做的所有 non-standard 更改都应在 InstallExecuteSequence 中的 InstallInitialize 和 InstallFinalize 之间完成 - 事务处理的提升操作,其中只有延迟模式和提升的自定义操作可以 运行。

    • 当您 运行 处于静默模式时,InstallUISequence 中所做的所有更改也将被完全跳过,然后安装可能会不完整。静默安装对于企业部署极为重要 - 通常总是忽略 GUI,并通过使用转换 and/or 从命令行设置属性来强制执行更改。

    • 这里有一个冗长的讨论,讨论静默和交互式安装和卸载如何产生不同的结果(以及它如何成为一个严重的 MSI 设计缺陷):Uninstall from Control Panel is different from Remove from .msi

  • 从不在 InstallExecuteSequence 的自定义操作中显示对话框。这样做会导致静默安装完全失败,因为这些对话框不会自动遵循 运行ning 安装的 UILevel 设置。当通过部署系统在静默模式下设置 运行 时,模式对话框会出现并阻止设置的完成,当然不会有用户关闭对话框。您可以使用 属性 UILevel 来确定设置是否为 运行 静默,然后抑制对话框的显示 - 但显示这样的对话框只是错误的设计。

11。 您尝试使用 MSI 安装程序“强制覆盖”文件

MSI 具有一些非常复杂的“file versioning rules”,旨在最大限度地减少“DLL Hell”的影响。它们通常会导致文件未按预期被覆盖 - 一个典型的 MSI 问题。因此,人们觉得他们无法找到一种可靠的方法来在安装过程中始终强制覆盖磁盘上的文件。

  • 有一些方法可以强制覆盖文件,但不是大多数人想象的合乎逻辑的方式。坦率地说,即使理解了文件替换设计,也常常不被接受。

  • 版本化文件和数据文件(文本、图像、任何没有版本的文件 属性)覆盖文件的方式完全不同。本质上,当文件版本化时,较高版本的文件会覆盖较低版本的文件。如果相关文件的创建日期和修改日期不同,则不会替换数据文件。自安装后对其进行了修改。

  • 可以通过 msiexec.exe 命令行级别的 REINSTALLMODE property 自定义设置稍微调整文件覆盖行为(覆盖旧版本、覆盖相同版本、覆盖任何版本等...)。设置 REINSTALLMODE 属性 更改整个设置中所有文件的文件替换逻辑 - 包括使用合并模块部署的文件,这些文件可能以共享位置中的文件为目标。因此,您可以降级共享文件和组件——这正是“DLL 地狱”的含义。

  • 尽管如此,了解“文件覆盖规则”以及它们如何受到设置的影响至关重要 但它是适用于整个安装中所有文件的设置。还有一些“黑客”只覆盖特定文件。

  • Check this article for how you can force overwrite a file that won't upgrade.

  • 这部分还没有结束。

12。 您安装 运行 具有用户凭据的服务

在我看来,这不是好的做法,通常人们也会在主要升级场景中清除凭据 - 在某些情况下还会清除服务使用的设置文件。

  • 对我来说,这是一个很好的例子,说明如何需要更改应用程序设计才能进行部署可靠而理智。

  • 根据我的经验,人们坚持使用这些解决方案并最终进行了大量自定义操作黑客攻击以使其正常工作。

  • 为自己省去很多麻烦,将 运行 的服务设计为本地系统 (or maybe better - another account that is intended for service use - do have a quick read of this linked content and talk to your development team about options. Here is another post that might be worth a skim: Is it safe to run a pool under NT AUTHORITY\NETWORK SERVICE?)。

  • 请参阅下一节关于 NT 权限的常见问题,了解将用户凭据用于 运行 服务时出现的常见问题。

  • 更新managed service accounts should also be mentioned. Step-by-step (also see section in this answer on managed and group service accounts的新概念)。

13。 您的应用程序需要广泛的自定义 NT 权限

NT privileges 不同于自主访问控制(文件系统和注册表对象的访问控制),包括诸如 SeServiceLogonRight“作为服务登录”(必须为任何尝试 运行 服务的用户帐户设置 - 对于尝试使用用户凭据 运行 服务的设置来说,这是一个非常常见的设置问题。

在某些情况下,运行 应用程序或更可能是服务需要过多的此类特权。非常强烈的“部署气味”或实际上是“解决方案气味”- anti-pattern 如果有的话。

几乎所有这些特权都是挥霍无度的危险

  • 我想 SeSystemtimePrivilege - 设置系统时间不是太关键 - 至少从表面上看是这样,但我真的没有看到任何完全无害的特权,除了上面提到的服务登录权限外,几乎不需要其他权限。

  • 根据我的经验,请求的权限往往围绕“Logon User Rights”。 SeNetworkLogonRight(从网络访问计算机), SeInteractiveLogonRight(本地登录),SeBatchLogonRight(作为批处理作业登录)和大的:SeServiceLogonRight(作为服务登录)。

  • 某些 NT 权限,例如 SeAssignPrimaryTokenPrivilegeSeBackupPrivilegeSeDebugPrivilegeSeIncreaseQuotaPrivilegeSeTchPrivilege(作为操作系统的一部分)和其他几个 永远不应被任何理智的软件包应用.

  • 用于运行服务的本地系统帐户拥有大部分权限(包括危险权限),应该用于运行 您的解决方案,而不是创建一个单独的用户帐户并将这些权限分配给它。 认真的.

  • 这里有一个很好的“grouped list of NT privileges”,它提供了更多的上下文来理解每个特权的用途以及它们之间的关系。

14。 您应用了很多自定义磁盘和注册表权限

这是明确的“部署气味”或部署“anti-pattern”。在几乎所有情况下,这都可以通过重新设计相关应用程序来避免。

  • 应用自定义权限传统上是使用各种命令行工具完成的。 MSI 中也有 built-in 功能可以做到这一点,但它们缺乏灵活性。

  • 随着 WiX 的出现,应用权限现在相对可靠,因为它是由了解 MSI 的开发人员制定的经过适当测试的解决方案。商业工具当然也支持自定义权限。

  • 在我看来,自定义权限仍然是您正在安装的软件有问题的标志,但我自己也应用了很多自定义权限。

  • 我经常看到 重复 self-repair 问题 由错误的权限应用到磁盘或注册表引起:(第 5 节) .

  • 我也看到过一些情况,如果不对失败的 ACL 权限进行一些认真的调整,错误的权限应用会导致卸载变得不可能。非常粗糙的工作,并且很容易通过尝试部署和自动修复而变得更糟。

  • 另一个明显的问题是您通过打开对机器上 per-machine 个位置的写访问权限引入的安全风险。

15。您在注册表中的许可证密钥在升级时重置

一个非常常见的设计是使用 MSI 组件将许可证密钥写入注册表。这可以是 HKCU 或更常见的 HKLM - 以使其成为同一台机器上所有用户的共享许可证。

如果您使用 MSI public 属性 设置此许可证密钥,您应该在全新安装时读回此值以确保您不要用空字符串覆盖那里的现有数据。 MSI public 属性(令人惊讶地)不会被持久化,并会被您的升级设置自动读回n 重大升级场景。忘记执行此操作是人们看到他们的许可证密钥在重大升级期间被清除的一个很常见的原因。

我很少(如果有的话)推荐 read/write 自定义操作。它们很容易出错,而且要正确处理起来可能很复杂——而且大多数人从来没有实施过正确的回滚(如果设置崩溃并需要回滚)。然而,你也有更多的权力通过自定义操作检查系统的“当前状态”,你可以调整你的自定义操作,使其始终 运行s,即使在修补序列期间,你可以让它做不同的事情如果需要,可以在不同的序列中处理事情。大多数时候,自定义操作 运行 在非预期的情况下实际上可能是个问题 - 例如在补丁安装期间。很少有人记得使用 NOT PATCH 来调节他们的自定义操作(以防止 运行 在修补过程中 ning)。

尽管如此,如果我被指示在安装过程中写入许可证,我可能会在安装过程中使用自定义操作将许可证密钥写入 HKLM。然而,这很重要,我宁愿从设置中完全删除整个许可问题,原因有很多这里描述:Installer with Online Registration for Windows Application(推荐阅读 - 有很多理由将许可排除在您的设置之外)。

16。不受欢迎的硬编码 GUID

一些 GUID 可以硬编码在您的 WiX 源文件(或其他 MSI 创建工具)中。例如组件 GUID - 对于每个组件,它们应该保持为 stable,除非您更改安装位置。此处尝试解释其基本原理:Change my component GUID in wix?

但是,不要硬编码包代码。对于每个构建,MSI 的包代码应始终为 auto-generated。它应该是独一无二的。更详细;包 GUID 的想法是它对于每个编译的 MSI 文件应该是唯一的。它只是用来唯一标识一个文件。根据定义,Windows 安装程序会将具有相同包 GUID 的两个不同 MSI 文件视为相同文件。各种X-files问题就这样产生了。因此,包 GUID 应始终为 auto-generated,因为它应该是唯一的。

许多人还 auto-generate 产品代码 - 因为他们只使用主要升级来升级他们的应用程序。为此 use-case auto-generated 产品代码工作得很好。但是,如果您还需要支持 Windows 安装程序小升级,您应该硬编码您的产品代码并在适当的时候更新它。 升级码一般应该是hard-coded,手动管理。 .

17。错误包含敏感数据

现在有一个单独的 Q/A 关于防止敏感数据出现在最终安装程序中的主题:

基本上建议是给你的文件一个 once-over 用于 硬编码 dev-box sins如何查看?我不喜欢它,用 Orca 打开 MSI - 然后 浏览 tables。最易受攻击的 table 可能是:RegistryPropertyIniFile,也许 Directory,如果您使用 MSI GUI:all tables relating to GUI。任何脚本(CustomAction table 或 Binary table - 后者要求您流出任何脚本 - 或在其源位置检查它们)。


链接:

  • (常见的重大升级问题)。