在 MSI 卸载期间如何删除文件



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

有关安装和升级过程,MSDN 上提供了可靠的文档(例如,请参阅文件版本控制规则和默认文件版本控制)。

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

所以,我的问题很简单:我想知道文件何时从系统中删除(情况并非总是如此 - 例如,该文件是否存在/保留SharedDLLRefCount)。

我找到的最接近的是以下MSDN链接,它提供了一些建议,但基本上是:"自己测试"。 这对我来说并不令人满意,因为我想知道在我向客户发送任何使用此行为的安装程序之前,我是否可以依赖 MSI 的 - 也许是当前的 - 行为。

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

  • 在哪些情况下 - 除了明确的"永久"定义或使用SharedDllRefCount- 文件/组件在卸载操作中可以幸存下来?

  • 如果 DLL 现在的版本高于安装时的版本(由于热修补等),是否会安全地将其删除?注意:我对此进行了测试,目前的答案是肯定的,但我需要知道这是否是预期行为以及我是否可以依赖它。

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

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

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

键路径:本质上每个组件都有一个">键路径" - 单个文件或注册表项/值,用于确定组件是否已安装。MSI 的总体概念是,此绝对组件密钥路径与唯一组件 GUID 之间存在1-to-1 mapping。GUID 实质上引用对该绝对路径进行计数。几年前我在回答中对此进行了解释,对于人们来说,这似乎是一个易于理解的解释,也许可以快速阅读以更详细地理解此组件引用:在 wix 中更改我的组件 GUID?

合并模块:此组件 GUID(用于该特定绝对磁盘或注册表位置)应由寻求部署相关文件或组件的所有设置使用。窗口安装程序允许此操作的机制称为"合并模块"。这是一个部分 MSI 数据库,可以在生成时合并到多个 MSI 文件中 - 允许在 MSI 文件之间共享相同的组件,其中包含所有组件中使用的正确组件 GUID,以便可以进行引用计数。这允许这些共享组件在每次由不同产品安装时递增引用计数,然后组件将保留在系统上,直到使用它的产品按顺序卸载时引用计数减少到 0。应该注意的是,如果HKLMSOFTWAREMicrosoftWindowsCurrentVersionSharedDLLs处的旧 ref 计数器不是同时为 0,则不会卸载该组件。如果组件设置为永久或安装时为空白组件 GUID(一种"安装并忘记"组件的特殊功能 - 它永远不会再处理),则也不会卸载它。

GUID:再次重复,一个GUID用于一个绝对路径(一个 GUID 用于规则所有路径) -一个 GUID具有关联的引用计数器,该计数器计算已注册组件的产品数及其关联的绝对键路径以供其使用。因此,例如,三个产品可能会注册某个组件 GUID 的使用,使其引用计数为 3,从而使其关联的键路径文件或注册表值一直存在,直到卸载所有 3 个产品。

SharedDLL已启用?:请注意,旧版 SharedDLL 引用计数器不一定为您的 MSI 组件启用。某些工具(如Installshield)启用标志以增加已安装的所有文件的旧版共享 DLL 引用计数器,并且您实际上必须为每个组件关闭它才能摆脱此行为。这与其他工具(例如WiX)形成鲜明对比,它不会默认为所有文件打开共享 dll 引用计数器(我不确定他们为哪些文件启用它 - 如果有的话)。高级安装程序也不会为所有组件启用 SharedDLL ref-count 标志(感谢Bogdan Mitrache验证这一点 - 请参阅下面的评论)。


弄乱了旧版参考计数器- 在开发和测试安装期间可能发生 - 可能会导致 Windows 安装程序 应卸载以意外保留在磁盘上的组件。 如果看到此消息,请检查干净的系统以确定是否搞砸了 传统引用计数器是主机上的问题。然后你需要 手动调整注册表以修复您的引用计数 开发机器。这将涉及本项下的所有适用项目 键:HKLMSOFTWAREMicrosoftWindowsCurrentVersionSharedDLLs。 这不是一份有趣的工作 - 我在使用 Installshield Developer 7 时发生了它 回到过去。

未能为每个绝对键路径保持一致的组件 GUID 将导致神秘且不可预测的问题,例如 MSI 卸载删除仍与其他产品共享的文件, 但是引用计数已经搞砸了。微星文件 错误地认为他们"拥有"共享组件并愉快地删除 他们。标识错误的情况(相同的绝对路径具有多个 指向它的组件 GUID - 每个引用计为 1)。 这是人们使用Windows安装程序面临的关键问题之一 - 因此,建议坚持每个组件一个文件。


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

  1. 你已经差不多回答了这个问题。如果 MSI 文件在卸载时减少其"注册"后,组件的引用计数(对于组件 GUID)大于 0,则该文件将保留在磁盘上。如果其 MSI 组件设置为永久组件,或者如果它具有空白组件 GUID,或者如果为组件启用了旧版 SharedDLL 引用计数(可能不是),并且此处的引用计数高于 0:HKLMSOFTWAREMicrosoftWindowsCurrentVersionSharedDLLs,它也将保留。这些都是我知道的情况。我想还有其他方面,例如广告产品,但老实说,我不确定它们将如何影响卸载。广告产品并没有真正安装,而是由用户按需"安装"。阅读 Phil 的回答,我还记得传递组件也可以按照他在答案中描述的方式卸载 - 通过在重新安装期间将相关条件评估为 false。

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

请尝试阅读此答案,因为它似乎已经为其他人澄清了事情:在Wix中更改我的组件GUID?(与上面建议的相同)。


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

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

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

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

:a) 没有剩余的 SharedDllRefcount。

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

c) 组件设置为可传递,并执行安装/维护操作,将关联的属性值设置为"false"。同样,这对系统是粘性的,而不是项目设置。

d) 它尚未标记为 msidbComponentAttributes由安装共享。

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

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

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

因此,在包含所有相同组件 ID 的最简单的升级情况下,不会删除任何文件:如果组件 ID、B、C 和 D 位于较旧的已安装产品中,并且组件 IDA、B、C 和 D 位于新升级中,则应用文件版本控制规则,递减 ref 计数会将文件保留在那里, 也许使用更高版本,当 REP 删除旧产品时。这就是为什么在进行此类升级或修补时必须遵循组件规则的原因,或者通过使用重新安装模式=vomus 重新安装=ALL进行升级。

如果组件 ID、B、C 和 D 位于较旧的已安装产品中,并且组件 IDA、B、C 和 E 位于新升级中,则同样的情况将被删除,但 D 将被删除,因为它的引用计数现在为零,假设没有其他客户端并且上述规则不适用。

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

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

最新更新