Search code examples
pythonpython-2.7comwin32comminitab

Ensure that process started by COM connection is killed


I'm automating Minitab 17 using Python's win32com library, and while all of commands execute correctly, I can't seem to get the process started by the Minitab process to exit when my script ends. My structure looks like

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()

I don't get any exceptions raised or error messages printed, the Mtb.exe process is still listed in task manager. Even more frustrating is if I run the following in an IPython session:

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

The Minitab process is closed immediately. I observe the same behavior in a normal python interactive session. Why would the process get closed correctly when running in an interactive session but not in a standalone script? What is done differently there that isn't being performed in my script?

I've also tried running process_data in a threading.Thread and in a multiprocessing.Process with no luck.

EDIT:

If I have a script containing nothing but

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

then when I run it I see the Mtb.exe process in task manager, but once the script exits the process is killed. So instead my question is why does it matter if this COM object is declared at top-level vs. inside a function?


Solution

  • The problem was that the the garbage collector could clean up the reference to the underlying IUnknown object (the base type for all COM objects), and without the gc doing it's job the process stayed alive. I solve the problem by using the weakref module to immediately wrap the COM object in a weakref so it could be more easily deferenced:

    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()
    

    I'm not sure I understand fully why this makes a difference, but I believe it's because there's never a direct reference to the object, only a weak reference, so all the functions that use the COM object only do so indirectly, allowing the GC to know that the object can be collected sooner. For whatever reason it still needs to be created at the top level of the module, but this at least makes it possible for me to write more reusable code that cleanly exits.