4

Hello :) I´m a complete beginner when it comes to COM objects, any help is appreciated!

I´m working on a Python program supposed to read incoming MS-Word documents in a client/server fashion, i.e. the client sends a request (one or multiple MS-Word documents) and the server reads specific content from those requests using pythoncom and win32com.

Because I want to minimize waiting time for the client (client needs a status message from server, I do not want to open an MS-Word instance for every request. Hence, I intend to have a pool of running MS-Word instances from which the server can pick and choose. This, in turn, means I have to reuse those instances from the pool in different threads and this is what causes trouble right now. After I read Using win32com with multithreading, my dummy code for the server looks like this:

import pythoncom, win32com.client, threading, psutil, os, queue, time, datetime

appPool = {'WINWORD.EXE': queue.Queue()}

def initAppPool():
    global appPool
    wordApp = win32com.client.DispatchEx('Word.Application')
    appPool["WINWORD.EXE"].put(wordApp) # For testing purpose I only use one MS-Word instance currently

def run_in_thread(appid, path):
    #open doc, read do some stuff, close it and reattach MS-Word instance to pool
    pythoncom.CoInitialize()
    wordApp = win32com.client.Dispatch(pythoncom.CoGetInterfaceAndReleaseStream(appid, pythoncom.IID_IDispatch))
    doc = wordApp.Documents.Open(path)
    time.sleep(3) # read out some content ...
    doc.Close()
    appPool["WINWORD.EXE"].put(wordApp)

if __name__ == '__main__':
    initAppPool()

    pathOfFile2BeRead1 = r'C:\Temp\file4.docx'
    pathOfFile2BeRead2 = r'C:\Temp\file5.doc'

    #treat first request
    wordApp = appPool["WINWORD.EXE"].get(True, 10) 
    pythoncom.CoInitialize()
    wordApp_id = pythoncom.CoMarshalInterThreadInterfaceInStream(pythoncom.IID_IDispatch, wordApp) 
    readDocjob1 = threading.Thread(target=run_in_thread,args=(wordApp_id,pathOfFile2BeRead1), daemon=True)
    readDocjob1.start() 

    #wait here until readDocjob1 is done 
    wait = True
    while wait:
        try:
            wordApp = appPool["WINWORD.EXE"].get(True, 1)
            wait = False
        except queue.Empty:
            print(f"[{datetime.datetime.now()}] error: appPool empty")
        except BaseException as err:
            print(f"[{datetime.datetime.now()}] error: {err}")
    

So far everything works as expected, but when I start a second request similar to the first one:

(x) wordApp_id = pythoncom.CoMarshalInterThreadInterfaceInStream(pythoncom.IID_IDispatch, wordApp)
    readDocjob2 = threading.Thread(target=run_in_thread,args=(wordApp_id,pathOfFile2BeRead2), daemon=True)
    readDocjob2.start()

I receive the following error message: "The application called an interface that was marshaled for a different thread" for the (x) marked line.

I thought that is why I have to use pythoncom.CoGetInterfaceAndReleaseStream to jump between threads with the same COM object? And besides that why does it work the first time but not the second time?

I searched for different solutions on StackOverflow which use CoMarshalInterface instead of CoMarshalInterThreadInterfaceInStream, but they all gave me the same error. I´m really confused right now.

EDIT: After fixing the error as mentioned in the comments, I ran into a mysterious behavior. When the second job is executed:

    wordApp_id = pythoncom.CoMarshalInterThreadInterfaceInStream(pythoncom.IID_IDispatch, wordApp)
    readDocjob2 = threading.Thread(target=run_in_thread,args=(wordApp_id,pathOfFile2BeRead2), daemon=True)
    readDocjob2.start()

The function run_in_thread terminates immediately without executing any line, respectively it seems that the pythoncom.CoInitialize() is not working properly. The script finishes without any error messages though.

def run_in_thread(instance,appid, path):
    #open doc, read do some stuff, close it and reattach MS-Word instance to pool
    pythoncom.CoInitialize()
    wordApp = win32com.client.Dispatch(pythoncom.CoGetInterfaceAndReleaseStream(appid, pythoncom.IID_IDispatch))
    doc = wordApp.Documents.Open(path)
    time.sleep(3) # read out some content ...
    doc.Close()
    instance.flag = True

NB149
  • 103
  • 6
  • You put back in the activePool a COM reference that you got from CoGetInterfaceAndReleaseStream (so a reference created specially for this new thread) and then you call CoMarshalInterThreadInterfaceInStream on this reference. That's what is wrong. You must always use the original COM reference got from the thread that created it to be able to call CoMarshalInterThreadInterfaceInStream. So you must change how your apppool works, use some kind of a "in use" flag but don't touch the original COM reference. – Simon Mourier Mar 25 '22 at 15:04
  • Thank you very much! Putting the "appPool["WINWORD.EXE"].put(wordApp)" outside of the function fixed the problem for me. As you said I have to adjust the appPool now. – NB149 Mar 28 '22 at 08:18
  • You should not chain questions one after the other in the same post. Create another question for your newer question with new code, etc. – Simon Mourier Mar 29 '22 at 08:00
  • Okay sorry and thanks for the advice :) – NB149 Mar 29 '22 at 09:16

1 Answers1

1

What happens is you put back in the "activePool" a COM reference that you got from CoGetInterfaceAndReleaseStream. But this reference was created specially for this new thread and then you call CoMarshalInterThreadInterfaceInStream on this new reference.

That's what is wrong.

You must always use the original COM reference you got from the thread that created it, to be able to call CoMarshalInterThreadInterfaceInStream repeatedly.

So, to solve the problem, you must change how your apppool works, use some kind of a "in use" flag but don't touch the original COM reference.

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298