代码之家  ›  专栏  ›  技术社区  ›  BudBrot

在MSI卸载过程中,文件究竟是如何删除的?

  •  2
  • BudBrot  · 技术社区  · 7 年前

    我想知道在卸载过程中,安装的文件/组件会发生什么情况。

    有关安装和升级过程,MSDN上有可靠的文档(请参阅 File Versioning Rules Default File Versioning ,例如)。

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

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

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

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

    • 在何种情况下-除了明确的“永久”定义或使用 SharedDllRefCount -文件/组件是否能在卸载操作后存活?

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

    2 回复  |  直到 7 年前
        1
  •  4
  •   Stein Åsmul    4 年前

    组件引用 对于MSI文件,是在Windows Installer组件的基础上完成的,而不是基于在注册表中找到的旧SharedDLL ref计数: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLLs .

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

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

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

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

    GUID :再次重复, 一个绝对路径的一个GUID (一个GUID来管理所有组件)-一个GUID有一个关联的ref计数器,该计数器统计已注册组件的产品数量及其关联的绝对密钥路径,以供使用。因此,作为一个示例,三个产品可能会注册某个组件GUID的使用,使其引用计数为3,从而使其关联的键路径文件或注册表值保持不变,直到所有3个产品都卸载为止。

    是否启用共享DDLL? :请注意,对于MSI组件,不一定要启用旧的SharedDLL ref计数器。一些工具,如 只有一句话 ,启用一个标志以递增所有已安装文件的旧共享DLL引用计数器,实际上,您必须为每个组件关闭该标志才能消除此行为。这与其他工具不同,例如 WiX公司 ,这不会默认所有文件的共享dll引用计数器都处于打开状态(我不确定它们启用了哪些文件-如果有的话)。 安装包制作 也不会为所有组件启用SharedDLL ref count标志(由于 Bogdan Mitrache公司 要验证这一点,请参见下面的评论)。


    混乱的旧引用计数器 -这可能发生在开发和测试安装期间-可能会导致Windows Installer 应卸载的组件意外留在磁盘上。 如果你看到了这一点,请检查干净的系统,以确定是否有问题 旧的ref计数器是主机上的问题。然后你需要 手动调整注册表以修复 开发机器。这将涉及本协议项下的所有适用项目 密钥: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLS . 这不是一份有趣的工作-我在使用Installshield Developer 7时遇到过这种情况 很久以前。

    未能为每个绝对密钥路径保持一致的组件GUID将导致神秘和不可预测的问题,例如MSI 卸载并删除仍与其他产品共享的文件, 但是参考文献的统计被搞砸了。 MSI文件 错误地认为他们“拥有”共享组件并愉快地删除 他们身份错误的情况(同一绝对路径有多个 指向它的组件GUID-每个引用计数为1)。 这是Windows Installer面临的关键问题之一- 因此,建议每个组件使用一个文件。


    使现代化 :让我们具体谈谈你的具体问题。

    1. 你已经大致回答了这个问题。如果MSI文件在卸载时减少了其“注册”,则该文件将保留在磁盘上,前提是组件的引用计数(组件GUID)大于0。如果其MSI组件被设置为永久,或者如果其组件GUID为空,或者如果为该组件启用了旧版SharedDL引用计数(可能不是),并且此处的引用计数大于0,则它也将保留: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\SharedDLS . 这些就是我所知道的所有情况。我想还有其他方面,如广告产品,但我真的不确定它们将如何影响卸载。广告产品不是真正安装的,而是用户根据需要“可安装”的。读了菲尔的回答,我还记得 transitive components 也可以按照他在回答中描述的方式卸载,方法是在重新安装期间将相关条件计算为false。

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

    请务必阅读此答案,因为它似乎为其他人澄清了一些事情: 是否更改wix中的组件GUID? (与上述建议相同)。


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

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

        2
  •  2
  •   PhilDW    7 年前

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

    如果资源(文件)的组件id ref计数为零,并且:

    a) 它没有剩余的SharedDLRefCount。

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

    c) 组件被设置为可传递,并且发生了一个安装/维护操作,该操作将关联的属性值设置为“false”。同样,这对系统来说是粘性的,而不是项目设置。

    d) 尚未将其标记为安装共享的msidbComponentAttributesShared。

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

    如果产品A安装版本1,产品B安装版本2,然后卸载产品B,则版本化的共享文件不会还原为以前的二进制文件。根据定义(实际上并不总是如此),共享文件需要支持较旧的客户端。

    在“end”处使用RemoveExistingProducts(REP)进行主要升级期间,升级会对文件应用文件版本控制规则,并增加已安装组件的引用计数(如果组件是新的,则安装引用计数为1的组件)。在这种升级中,组件与旧产品已安装的相同组件ID有效共享。当REP卸载旧产品时,ref计数将减少。

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

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

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

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