Python中的msiexec脚本编写

0 投票
1 回答
885 浏览
提问于 2025-04-16 13:39

大部分内容是背景信息,接下来的三段可以跳过,直接看问题:

我开发了一个工具,它可以调用一些安装程序,修改注册表项,并移动文件,帮助我测试一个更新速度很快的产品。目前一切正常,我有一个图形界面(GUI),它和业务逻辑在不同的进程中运行,这样可以避免因为全局解释器锁(GIL)导致的卡顿,所有功能都正常。不过,我对代码中调用msiexec的部分有些担心。

具体来说,我担心的是卸载的部分。目前GUID(全局唯一标识符)没有变化,所以我可以用os.system('msiexec /x "{GUID}" /passive')这样的方式来卸载产品。其实这有点复杂,因为我使用了subprocess.Popen,并在事件循环中轮询它,直到它完成,这样可以和其他步骤并行进行。

我的担心是,如果GUID发生变化,这种方法就不管用了。我不想直接把msiexec指向安装源,因为如果我“丢失”了msi文件(我把它存放在临时目录中),这样就会出问题。

我想要的是一种通过程序名称查询来获取GUID的方法,或者一个可以为msiexec封装的工具,帮我完成所有这些操作,包括卸载。我考虑过扫描注册表,但_winreg模块似乎很慢,所以如果能避免的话我更愿意不使用它。如果有更好的方法来扫描注册表,我非常乐意听取,因为这也会加快工具的其他部分。


更新0

这个性能非常关键,因为设计目标之一是让工具的处理过程比任何其他方法(无论是手动还是其他方式)都要快,以便能够广泛使用。


更新1

我尝试了下面的注册表版本的一个小变种,但它总是返回None。我不太确定这是怎么回事——看起来是无法打开相应的键,因为我在with语句后插入的断点从来没有被触发过……

def get_guid_by_name(name):
  from _winreg import (OpenKey,
                       QueryInfoKey,
                       EnumKey,
                       QueryValueEx,
                       HKEY_LOCAL_MACHINE,
                       )
  with OpenKey(HKEY_LOCAL_MACHINE, 
               r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') as key:
      subkeys, _0, _1 = QueryInfoKey(key) # The breakpoint here is never reached
      del _0, _1
      for i in range(subkeys):
        subkey = EnumKey(key, i)
        if subkey[0] != '{' or subkey[-1] != '}':
          continue
        with OpenKey(key, subkey) as _subkey:
          if name in QueryValueEx(_subkey, 'DisplayName')[0]:
            return subkey
  return None

  print get_guid_by_name('Microsoft Visual Studio')

更新2

算了,我真是个傻瓜,没有仔细检查缩进——print get_guid_by_name('Microsoft Visual Studio')其实是在get_guid_by_name内部……

1 个回答

1

我觉得 _winreg 模块并没有那么慢。如果你想要遍历整个注册表去找某个字符串的所有实例,那可能会花不少时间,但如果你有个明确的查询,速度还是挺快的。

这里有个例子:

from _winreg import *

def get_guid_by_name(name):
    # Open the uninstaller key
    with OpenKey(HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows\CurrentVersion\Uninstall') as key:
        # We only care about subkeys of the installer key
        subkeys, _, _ = QueryInfoKey(key)
        for i in range(subkeys):
            subkey = EnumKey(key, i)
            # Since we're looking for uninstallers for MSI products,
            # the key name will always be the GUID. We assume that any
            # key starting with '{' and ending with '}' is a GUID, but
            # if not the name won't match.
            if subkey[0] != '{' or subkey[-1] != '}':
                 continue
            # Query the display name or other property of the key to
            # see if it's the one we want
            with OpenKey(key, subkey) as _subkey:
                if QueryValueEx(_subkey, 'DisplayName')[0] == name:
                    return subkey
     return None

在我的电脑上,查询 ActiveState 的 Komodo Edit(我其实用了正则表达式,而不是直接比较值),运行1000次这个查询花了8.18秒(用 timeit 测试的),我觉得这个时间几乎可以忽略不计。更棒的是,你可以直接从注册表中提取 UninstallString 这个键,然后把它传给你的子进程(不过你可能想在后面加上 /passive 这个参数)。


补充说明

当然,微软提供了一个 WMI 类(Win32_Product),可以很方便地完成这些操作。使用 Tim Golden 的优秀 WMI 封装,你可以这样启动安装:

import wmi
c = wmi.WMI()
c.Win32_Product(Name = 'ProductName')[0].Uninstall()

不过,正如这篇博客中提到的,Win32_Product 类的使用速度非常慢,真的是让人受不了。

撰写回答