Pythoncom PumpMessages from different thread

*爱你&永不变心* 提交于 2021-02-09 08:25:09

问题


I want to do something similar to what is asked here, but using threading like here. Using also the answer from here, I got my code working, only that an ItemAdd event is not recognised (actually, I think it is, but in the other thread, which is why there is no output).

"""Handler class that watches for incoming mails"""
import ctypes # for the WM_QUIT to stop PumpMessage()
import logging
import win32com.client
import sys
import threading
import time
import pythoncom

# outlook config
CENTRAL_MAILBOX = "My Mailbox"

# get the outlook instance and inbox folders
outlook = win32com.client.Dispatch("Outlook.Application")
marshalled_otlk = pythoncom.CoMarshalInterThreadInterfaceInStream(
    pythoncom.IID_IDispatch, outlook)


class HandlerClass(object):

    def OnItemAdd(self, item):
        logger.info("New item added in central mailbox")
        if item.Class == 43:
            logger.info("The item is an email!")


class OtlkThread(threading.Thread):

    def __init__(self, marshalled_otlk, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.marshalled_otlk = marshalled_otlk
        self.logger = logging.getLogger("OLThread")

    def run(self):
        self.logger.info("Starting up Outlook watcher\n"
                         "To terminate the program, press 'Ctrl + C'")
        pythoncom.CoInitialize()
        outlook = win32com.client.Dispatch(
            pythoncom.CoGetInterfaceAndReleaseStream(
                self.marshalled_otlk,
                pythoncom.IID_IDispatch
            )
        )
        user = outlook.Session.CreateRecipient(CENTRAL_MAILBOX)
        central_inbox = outlook.Session.GetSharedDefaultFolder(user, 6).Items
        self.logger.info(f"{central_inbox.Count} messages in central inbox")

        win32com.client.DispatchWithEvents(central_inbox, HandlerClass)
        pythoncom.PumpMessages()
        pythoncom.CoUninitialize()  # this is prbly unnecessary as it will never be reached


def main():
    # pythoncom.CoInitialize()
    OtlkThread(marshalled_otlk, daemon=True).start()


if __name__ == "__main__":
    status = main()
    while True:
        try:
            # pythoncom.PumpWaitingMessages()
            time.sleep(1)
        except KeyboardInterrupt:
            logger.info("Terminating program..")
            ctypes.windll.user32.PostQuitMessage(0)
            sys.exit(status)

I have tried various things, such as putting sys.coinit_flags=0 at the top, as suggested here), calling PumpWaitingMessages() in the main thread, and getting the Outlook application in the side thread itself, instead of passing the marshalled object. None of these have worked.

When I just put PumpMessages in the main thread (same HandlerClass but no separate thread) it works and emails are recognised upon arrival, but obviously the thread is blocked and not even the KeyboardInterrupt exception can be caught.

So, how do I get the outlook watcher running in the separate thread to send the messages to the main thread for output there?


回答1:


Nicely formatted question, with references. Thanks.

To the answer. I use ``threading.Thread(target= ....` in that cases. However you can use inheritance as well:

import logging
import threading
import time


def task(name):
    log = logging.getLogger('task-' + name)
    log.info("Starting up Outlook watcher\n"
                     "To terminate the program, press 'Ctrl + C'")
    while True:
        log.info("Doing work that takes time")
        time.sleep(1)


class OtlkThread(threading.Thread):

    def __init__(self, *args, **kwargs):
        self.log = logging.getLogger('task-class')
        super().__init__(*args, **kwargs)

    def run(self):
        self.log.info("Starting up Outlook watcher\n"
                 "To terminate the program, press 'Ctrl + C'")
        while True:
            self.log.info("Doing work that takes time")
            time.sleep(1)

def main():
    t1 = threading.Thread(target=task, args=('daemon',), daemon=True)
    t1.start()
    t2 = OtlkThread()
    t2.start()


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG,)

    main()
    while True:
        time.sleep(1)



回答2:


Again thanks a lot for your answer Michael, it led me to this answer which also contains a link to an excellent example. The main takeaway from the answer and the example is that, instead of passing Outlook as a marshalled object, just pass it as client to the handler. Also, to use WithEvents instead of DispatchWithEvents and to set sys.coinit_flags = 0 before importing pythoncom.

The final result looks like this:

"""Handler class that watches for incoming mails"""
import ctypes # for the WM_QUIT to stop PumpMessage()
import logging
import sys
import time
from threading import Thread

sys.coinit_flags = 0  # pythoncom.COINIT_MULTITHREADED == 0
from pythoncom import (CoInitializeEx, CoUninitialize,
                       COINIT_MULTITHREADED, PumpWaitingMessages)
from win32com.client import Dispatch, WithEvents


# outlook config
CENTRAL_MAILBOX = "My Mailbox"


# COM event handler
class IncomingMailHandler:
    def OnItemAdd(self, item):
        logger.info("New item added in central mailbox")
        if item.Class == 43:
            logger.info(f"The item is an email with subject {item.Subject}")


# main thread
def main():
    # get the outlook instance and inbox folders
    outlook = Dispatch("Outlook.Application")
    user = outlook.Session.CreateRecipient(CENTRAL_MAILBOX)
    central_inbox = outlook.Session.GetSharedDefaultFolder(user, 6).Items
    logger.info(f"{central_inbox.Count} messages in central inbox")

    # launch the second thread
    thread = Thread(target=watcher, args=(central_inbox,), daemon=True)
    thread.start()


# other thread worker function
def watcher(client):
    logger = logging.getLogger("watcher")
    CoInitializeEx(COINIT_MULTITHREADED)
    WithEvents(client, IncomingMailHandler)
    # event loop 2
    _loop = 0
    while True:
        PumpWaitingMessages()
        _loop += 1
        if _loop % 20 == 0:
            logger.info("Watcher is running..")
        time.sleep(0.5)
    CoUninitialize()


if __name__ == "__main__":
    logger.info("Starting up Outlook watcher\n"
                "To terminate the program, press 'Ctrl + C'")
    status = main()
    while True:
        try:
            time.sleep(0.5)
        except KeyboardInterrupt:
            logger.info("Terminating program..")
            ctypes.windll.user32.PostQuitMessage(0)
            sys.exit(status)


来源:https://stackoverflow.com/questions/54787608/pythoncom-pumpmessages-from-different-thread

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!