确保由 COM 连接启动的进程被终止

Ensure that process started by COM connection is killed

我正在使用 Python 的 win32com 库自动化 Minitab 17,虽然所有命令都正确执行,但我似乎无法让 Minitab 进程启动进程退出当我的脚本结束时。我的结构看起来像

from myapi import get_data

import pythoncom
from win32com.client import gencache

def process_data(data):
    # In case of threading
    pythoncom.CoInitialize()
    app = gencache.EnsureDispatch('Mtb.Application')
    try:
        # do some processing
        pass
    finally:
        # App-specific command that is supposed to close the software
        app.Quit()
        # Ensure the object is released
        del mtb
        # In case of threading
        pythoncom.CoUninitialize()

def main():
    data = get_data()
    process_data(data)

if __name__ == '__main__':
    main()

我没有收到任何异常或打印错误消息,Mtb.exe 进程仍在任务管理器中列出。更令人沮丧的是,如果我 运行 在 IPython 会话中执行以下操作:

>>> from win32com.client import gencache
>>> app = gencache.EnsureDispatch('Mtb.Application')
>>> ^D

Minitab 进程立即关闭。我在正常的 python 交互式会话中观察到相同的行为。 为什么 运行 在交互式会话中而不是在独立脚本中时,进程会正确关闭?我的脚本中没有执行哪些不同的操作?

我也在 threading.Threadmultiprocessing.Process 中尝试了 运行ning process_data,但没有成功。

编辑:

如果我的脚本只包含

from win32com.client import gencache
app = gencache.EnsureDispatch('Mtb.Application')

然后当我 运行 它时,我在任务管理器中看到 Mtb.exe 进程,但是一旦脚本退出,进程就会被终止。所以我的问题是,为什么这个 COM 对象是在顶层声明还是在函数内部声明很重要?

我没有 minitab,所以我无法验证,但尝试在调用 app.Quit 之后通过设置 app = None 来强制关闭 COM 服务器? Python 使用引用计数来管理对象生命周期,因此假设应用程序没有其他引用然后将其设置为 none 应该会导致它立即完成。我看到这会导致类似的问题。你不应该需要弱参考,其他事情正在发生。根据您的回答,以下内容应该有效:

def process_data(mtb, data):
    try:
        mtb.do_something(data)
    finally:
        mtb.Quit()

def main(mtb):
    data = get_data()
    process_data(mtb, data)

if __name__ == '__main__':
    pythoncom.CoInitialize()
    mtb = gencache.EnsureDispatch('Mtb.Application')
    main(mtb)
    mtb.Quit()
    mtb = None
    pythoncom.CoUninitialize()

问题是垃圾收集器可以清除对底层 IUnknown 对象(所有 COM 对象的基本类型)的引用,并且如果没有 gc 执行它的工作,进程就会保持活动状态。我通过使用 weakref 模块立即将 COM 对象包装在一个 weakref 中来解决这个问题,这样它就可以更容易地被引用:

from myapi import get_data

import weakref
from win32com.client import gencache
import pythoncom

def process_data(mtb_ref, data):
    try:
        mtb_ref().do_something(data)
    finally:
        mtb_ref().Quit()

def main(mtb_ref):
    data = get_data()
    process_data(mtb_ref, data)

if __name__ == '__main__':
    pythoncom.CoInitialize()
    mtb_ref = weakref.ref(gencache.EnsureDispatch('Mtb.Application'))
    main(mtb_ref)
    pythoncom.CoUninitialize()

我不确定我是否完全理解为什么这会有所不同,但我相信这是因为从来没有对对象的直接引用,只有弱引用,所以所有使用 COM 对象的函数都只这样做间接地,让 GC 知道可以更快地收集该对象。无论出于何种原因,它仍然需要在模块的顶层创建,但这至少使我可以编写更多可重用的代码,这些代码可以干净地退出。

在 pythoncom.CoUninitialize() 之后我仍然看到进程
对我有帮助 (based):

from comtypes.automation import IDispatch
from ctypes import c_void_p, cast, POINTER, byref

def release_reference(self, obj):
    logger.debug("release com object")
    oleobj = obj._oleobj_
    addr = int(repr(oleobj).split()[-1][2:-1], 16)

    pointer = POINTER(IDispatch)()
    cast(byref(pointer), POINTER(c_void_p))[0] = addr
    pointer.Release()