在 MSI 卸载期间文件究竟是如何删除的?

How exactly are files removed during MSI uninstall?

我想知道在卸载过程中安装的文件/组件到底发生了什么。

对于安装和升级过程,MSDN 上有可靠的文档(例如,参见 File Versioning Rules and Default File Versioning)。

无论如何,我无法在 MSDN 或 WiX 的文档中找到卸载删除逻辑的文档。

所以,我的问题很简单:我想知道文件何时从系统中删除(情况并非总是如此 - 例如,如果 SharedDLLRefCount exists/remains那个文件)。

我找到的最接近的是以下 MSDN link,它提供了一些建议,但基本上是这样说的:"Test it yourself"。 这对我来说并不令人满意,因为我想知道在我将使用此行为的任何安装程序发送给客户之前,我是否可以依赖 MSI 的一种(也许是当前的)行为。

我正在寻找以下问题的可靠答案:

MSI 文件的组件引用 是在 Windows 安装程序组件的基础上完成的 - 而不是基于在此处的注册表中找到的旧 SharedDLL 引用计数:HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLLs.

SharedDLL:奇怪的是,这个 SharedDLL 引用计数器有时也与 MSI 一起使用,但这只是为了提供与旧版安装程序的兼容性和部署技术——稍后我会澄清。传统技术使用此 SharedDLL 计数器作为确定文件是否可以卸载的唯一方法。一旦 ref-count 降为 0,就可以删除该文件。

Windows 安装程序的实际引用计数是基于 Windows 安装程序组件而不是共享 dll 引用计数器。这些组件是文件和注册表设置的“原子安装包”。它总是作为一个整体安装或卸载。一个组件基本上可以包含“任何东西”,但是在分解要部署到组件集合中的文件和注册表设置时,有一些关于最佳实践的规则。就个人而言,我总是每个组件使用一个文件,因为这避免了 Windows 安装程序升级期间的各种问题 .

Key Path:本质上每个组件都有一个“key-path”——使用的单个文件或注册表项/值以确定组件是否已安装。 MSI 的整体概念是在这个绝对组件键路径和唯一组件 GUID 之间有一个 1-to-1 mapping。 GUID 本质上是对绝对路径的引用计数。几年前我在一个答案中对此进行了解释,这对人们来说似乎是一个易于理解的解释,也许可以快速阅读以更详细地了解该组件引用:Change my component GUID in wix?

合并模块:该组件 GUID - 对于特定的绝对磁盘或注册表位置 - 应该被所有寻求部署有问题的文件或组件。 Window 安装程序允许这样做的机制称为 “合并模块”。这是一个部分 MSI 数据库,可以在构建时合并到多个 MSI 文件中 - 允许在 MSI 文件之间共享相同的组件,并在所有文件中使用正确的组件 GUID,以便可以进行引用计数。 这允许这些共享组件在每次被不同的产品安装时增加引用计数,然后该组件将保留在系统上,直到引用计数减少到 0,因为使用它的产品是依次卸载。需要注意的是,如果HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLLs处的legacy ref-counter同时不为0,那么该组件将不会被卸载。如果该组件被设置为永久性或如果它是使用空白组件 GUID 安装的(“安装并忘记”组件的特殊功能 - 它永远不会再次处理),也不会被卸载。

GUIDs:因此再次重复,一个绝对路径的一个 GUID(一个 GUID 来统治它们)- 一个 GUID 有一个关联的引用计数器,它计算已注册该组件的产品数量,及其关联的绝对键路径,供其使用。因此,举个例子,三个产品可能会注册某个组件 GUID 的使用,使其引用计数为 3,从而使其关联的密钥路径文件或注册表值一直存在,直到所有 3 个产品都被卸载。

SharedDLL Enabled?:请注意,您的 MSI 组件不一定启用旧版 SharedDLL 引用计数器。一些工具,例如 Installshield,启用一个标志来增加所有已安装文件的遗留共享 DLL 引用计数器,实际上您必须为每个组件关闭它才能摆脱这种情况行为。这与其他工具形成对比,例如 WiX,它不会默认为所有文件启用共享 dll 引用计数器(我不确定他们为哪些文件启用它- 如果有的话)。 Advanced Installer 也不会为所有组件启用 SharedDLL 引用计数标志(感谢 Bogdan Mitrache 对此进行了验证 - 请参阅下面的评论)。


Messed up legacy reference counters - which can happen during development and test installation - may cause a Windows Installer component that should be uninstalled to be left on disk unexpectedly. If you see this, check on a clean system to determine if messed up legacy ref-counters is the problem on your main machine. You then need to manually tweak the registry to fix the ref-count for your development machine. That will involve all applicable items under this key: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLLs. Not a fun job - I had it happen when using Installshield Developer 7 way back in the day.

Failing to keep a consistent component GUID for each absolute key path will cause mysterious and unpredictable problems such as an MSI uninstall removing files that are still shared with other products, but the reference counting has been messed up. The MSI files mistakenly believe they "own" the shared component and happily deletes them. A case of mistaken identity (the same absolute path has multiple component GUIDs pointing to it - each one reference counted to 1). This is one of the key problems people face with Windows Installer - hence the advice to stick with one file per component.


更新:让我们具体说明您的具体问题。

  1. 你已经差不多回答了这个问题。如果组件的引用计数(对于组件 GUID)高于 0,则在您的 MSI 文件在卸载时减少其“注册”后,该文件将保留在磁盘上。如果它的 MSI 组件被设置为永久的,或者如果它有一个空白的组件 GUID,或者如果为该组件启用了遗留的 SharedDLL 引用计数(它可能不是)并且这里的引用计数更高,它也会保留比 0:HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLLs。这些就是我所知道的所有条件。我想还有其他方面,例如广告产品,但老实说,我不确定它们将如何影响卸载。广告产品并没有真正安装,而是用户按需“安装”。阅读 Phil 的回答我还记得 transitive components 也可以按照他在回答中描述的方式卸载 - 通过在重新安装期间将相关条件评估为 false。

  2. 是的,只要组件 GUID 在特定绝对路径(文件的完整安装路径)的整个生命周期内保持稳定,那么该文件就可以经历任意数量的更新,并且仅当具有另一个产品代码的另一个 MSI 也安装了它时,引用计数才会增加。换句话说:如果您已经向原始 MSI 交付了 4 次更新,并且您为特定文件维护了一致的组件 GUID 并且每次都使用新版本更新它,那么该文件的组件引用计数仍然是 1 -只要没有其他 MSI 也安装了该组件 - 在这种情况下它将是 2 个或更多,并且卸载您的产品将 不会 卸载它,但会减少引用计数1.

请尝试阅读这个答案,因为它似乎已经为其他人澄清了一些事情:Change my component GUID in wix?(与上面的推荐相同)。


最后我要注意 共享组件 也可以通过 WiX 包含文件 安装 - 一个完全随 WiX 引入的新概念。这些就像 C++ 中的常规包含文件,您只需定义它们一次,它们就可以在编译时包含在多个 WiX 源文件中。老实说,我从未使用过它,但从概念上讲,它类似于合并模块 - 内置 Windows 安装程序概念来处理共享组件。但是有一个重要的区别:合并模块作为一个整体进行版本控制,而 WiX 包含文件包含来自源文件夹的动态文件。我觉得这样更好,但这肯定是一个很大的讨论,也是一个偏好问题。这里就不细说了。

如果您使用的是 WiX,我建议您尝试使用 WiX 包含文件来管理您的共享组件。在我看来,它们似乎是合并模块的更灵活的实现。主观上,我从来都不是合并模块的忠实拥护者,尽管如果您有很多共享文件要与不同的产品一起安装,那么使用它们是必不可少的。为什么我不喜欢合并模块?它们看起来像是一个额外的并发症和一个需要额外维护的二进制 blob。实际上,它们相当于一种奇怪的静态链接形式——具有我们从常规静态链接中了解到的所有问题。这可能太主观了,所以我将以该注释结束,但对于共享文件和组件,使用合并模块或 WiX 包含文件或任何其他存在的结构来实现我不知道的相同目的。

确实只有少数规则适用,但困难在于它们适用于许多有时很复杂的场景。

如果资源(文件)的组件 ID 引用计数变为零并且:

,则该资源(文件)将被删除

a) 没有剩余的 SharedDllRefcount。

b) 该组件从未在安装 MSI 时被标记为永久(因为这会使它粘住并且无法关闭)。

c) 组件被设置为可传递的,并且发生 install/maintenance 操作,将关联的 属性 值设置为 "false"。同样,这是对系统的粘性,而不是项目设置。

d) 它没有被安装标记为 msidbComponentAttributesShared。

可以在组件上设置永久的、传递的、组件共享的和共享的Dll。

如果产品 A 安装版本 1,产品 B 安装版本 2,然后卸载产品 B,版本共享文件不会恢复到以前的二进制文件。根据定义(在实践中并不总是如此),共享文件需要支持较旧的客户端。

在 "end" 使用 RemoveExistingProducts (REP) 进行重大升级期间,升级应用文件的文件版本控制规则,并增加已安装组件的引用计数(或安装新组件)引用计数为 1)。在这种升级中,组件有效地与旧产品已安装的相同组件 ID 共享。当 REP 卸载旧产品时,引用计数会减少。

因此,在包含所有相同组件 ID 的升级的最简单情况下,不会删除任何文件:如果组件 ID A、B、C 和 D 位于较早安装的产品中,并且组件 ID A、B、C 和D 在新升级中,然后应用文件版本控制规则,并且当 REP 删除旧产品时,递减引用计数将文件留在那里,可能具有更高版本。这就是为什么组件规则必须遵循此类升级、补丁或通过使用 REINSTALLMODE=vomus REINSTALL=ALL 重新安装来升级的原因。

如果组件 ID A、B、C 和 D 在较早安装的产品中,而组件 ID A、B、C 和 E 在新升级中,那么会发生同样的情况,除了 D 将被删除,因为它的引用计数现在为零,假设没有其他客户并且上述规则不适用。

使用 REP "early" 的主要升级在最好的情况下很简单,因为它是先卸载旧产品,然后安装新产品,因此可以安装旧版本的文件,然后再次删除文件或不按上述规则。 None 的组件 ID 需要相同,最简单的情况是没有其他产品使用的共享文件。

常见问题涉及将组件设置为永久或共享 Dll(仅当文件与非 MSI 安装共享时才需要)。似乎可以通过进行另一个更改它们的安装来关闭这些设置,但这些是系统设置,而不是项目设置。