0

My python application stops responding whenever I do anything "wx" a few times...whether that be clicking buttons, menus, inside the text area, etc.

This seems to be a problem with my use of windows hooks elsewhere in the program. When the two are used together, things go bad.

While the example below might not make much sense outside the context of the real application, it is what I came up with as a minimal example. I took everything out I could in order to try to diagnose the problem with minimal extra complexities.

It really seems like the second windows message pump called from the worker thread is the factor that makes it happen. If I comment that out, the app doesn't hang...or do anything useful...Hooks requires a pump, and I can't put it on my UI thread or it will the hooks will timeout, which led me to this POC in the first place...

If you run this and click the mouse a dozen times or so, you will notice the text blocks get garbled and then the app just hangs. Does anyone have any insight as to why?

I am using

  • python 2.7.16 32 bit
  • pyHook 1.5.1
  • wxPython 3.0.2.0 32bit for python 2.7

(update) I've also tried with the same results

  • python 2.7.16 32 bit
  • pyHook 1.5.1
  • wxPython 4.0.6 32 bit for python 2.7

We use these in the real application, because the my higher ups want to continue to support Windows XP (A whole different conversation)

main.py

import wx
from twisted.internet import wxreactor

from windows_hooks import WindowsHooksWrapper
from main_window import MainWindow


def main():
    hook_wrapper = WindowsHooksWrapper()
    hook_wrapper.start()

    app = wx.App(False)
    frame = MainWindow(None, 'Hooks Testing', hook_wrapper)

    from twisted.internet import reactor
    reactor.registerWxApp(app)
    reactor.run()

    hook_wrapper.stop()


if __name__ == "__main__":
    wxreactor.install()
    main()

windows_hooks.py

import pyHook
import threading
import pythoncom


class WindowsHooksWrapper(object):
    def __init__(self):
        self.hook_manager = None
        self.started = False
        self.thread = threading.Thread(target=self.thread_proc)
        self.window_to_publish_to = None

        print "HookWrapper created on Id {}".format(threading.current_thread().ident)

    def __del__(self):
        self.stop()

    def start(self):
        if self.started:
            self.stop()

        self.started = True
        self.thread.start()

    def stop(self):
        if not self.started:
            return

        self.started = False
        self.thread.join()

    def on_mouse_event(self, event):
        """
        Called back from pyHooks library on a mouse event
        :param event: event passed from pyHooks
        :return: True if we are to pass the event on to other hooks and the process it was intended
         for. False to consume the event.
        """

        if self.window_to_publish_to:
            from twisted.internet import reactor
            reactor.callFromThread(self.window_to_publish_to.print_to_text_box, event)
        return True

    def thread_proc(self):
        print "Thread started with Id {}".format(threading.current_thread().ident)

        # Evidently, the hook must be registered on the same thread with the windows msg pump or
        #     it will not work and no indication of error is seen
        # Also note that for exception safety, when the hook manager goes out of scope, the
        #     documentation says that it unregisters all outstanding hooks
        self.hook_manager = pyHook.HookManager()
        self.hook_manager.MouseAll = self.on_mouse_event
        self.hook_manager.HookMouse()

        while self.started:
            pythoncom.PumpMessages()

        print "Thread exiting..."

        self.hook_manager.UnhookMouse()
        self.hook_manager = None

main_window.py

import threading
import wx


class MainWindow(wx.Frame):
    def __init__(self, parent, title, hook_manager):
        wx.Frame.__init__(self, parent, title=title, size=(800, 600))
        self.hook_manager = hook_manager

        self.CreateStatusBar()

        menu_file = wx.Menu()
        menu_item_exit = menu_file.Append(wx.ID_EXIT, "E&xit", " Terminate the program")

        menu_help = wx.Menu()
        menu_item_about = menu_help.Append(wx.ID_ABOUT, "&About", " Information about this program")

        menu_bar = wx.MenuBar()
        menu_bar.Append(menu_file, "&File")
        menu_bar.Append(menu_help, "&Help")
        self.SetMenuBar(menu_bar)

        self.panel = MainPanel(self, hook_manager)

        self.Bind(wx.EVT_MENU, self.on_about, menu_item_about)
        self.Bind(wx.EVT_MENU, self.on_exit, menu_item_exit)

        self.Show(True)

    def on_about(self, e):
        dlg = wx.MessageDialog(self, "A window to test Windows Hooks", "About Test Windows Hooks",
                               wx.OK)
        dlg.ShowModal()
        dlg.Destroy()

    def on_exit(self, e):
        self.Close(True)


class MainPanel(wx.Panel):
    def __init__(self, parent, hook_manager):
        self.hook_manager = hook_manager
        hook_manager.window_to_publish_to = self

        self.consuming = False

        wx.Panel.__init__(self, parent)
        self.textbox = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)

        self.horizontal = wx.BoxSizer()
        self.horizontal.Add(self.textbox, proportion=1, flag=wx.EXPAND)

        self.sizer_vertical = wx.BoxSizer(wx.VERTICAL)
        self.sizer_vertical.Add(self.horizontal, proportion=1, flag=wx.EXPAND)
        self.SetSizerAndFit(self.sizer_vertical)
        self.called_back_count = 0

    def print_to_text_box(self, event):
        self.called_back_count += 1
        print "Printing message {} on Thread with Id {}".format(self.called_back_count,
                                                                threading.current_thread().ident)
        self.textbox.AppendText('MessageName: {}\n'.format(event.MessageName))
        self.textbox.AppendText('Message: {}\n'.format(event.Message))
        self.textbox.AppendText('Time: {}\n'.format(event.Time))
        self.textbox.AppendText('Window: {}\n'.format(event.Window))
        self.textbox.AppendText('WindowName: {}\n'.format(event.WindowName))
        self.textbox.AppendText('Position: {}\n'.format(event.Position))
        self.textbox.AppendText('Wheel: {}\n'.format(event.Wheel))
        self.textbox.AppendText('Injected: {}\n'.format(event.Injected))
        self.textbox.AppendText('---\n')

I've also tried a version without Twisted and used wxPostEvent with a custom event instead, but we were suspecting that might be the problem, so I changed it to use twisted and it's still no good.

I'll post an edited listing with that in a bit.

Christopher Pisz
  • 3,757
  • 4
  • 29
  • 65
  • When the instance of `wx.App` is created a message or event loop is created. The native messages are redirected to this wx-loop which handles them and allows the user to do some actions (the `Bind()` thing). Using another API that also wants those messages will mess boths APIs. – Ripi2 Jul 10 '19 at 17:43
  • @ripi2. If the second pump is removed, the hooks no longer receive notifications at all. Also, The windows api GetMessage function is thread specific. – Christopher Pisz Jul 10 '19 at 22:33
  • What do you need from those hooks than can not be achieved by wxWigets? – Ripi2 Jul 10 '19 at 23:16
  • @ripi2 The ability to block user input headed for another process. It sounds malicious, but it really isn't. We want to play back input events after we capture them, and when we play them back we can't have the user giving input in the middle of the sequence. That's what led me to using hooks originally, anyway. Think an app akin to AutoIt, but smaller scale. When we return False from the hook, user input events are consumed. – Christopher Pisz Jul 11 '19 at 00:59
  • Again, I think this can easily done with wx. You can capture, consume and re-post user events. Controlling a process from another process is a bit more tricky, requires IPC or a tmp file as intermediate. – Ripi2 Jul 11 '19 at 12:48
  • @Ripi I am not trying to control a another process I wrote. I am trying to stop the user from sending any input to _any_ process at all until playback is complete. – Christopher Pisz Jul 11 '19 at 17:27
  • May be a language issue, when you say "process", do you refer to another control (e.g. a text control) in the same frame? Or to another app? Or? – Ripi2 Jul 11 '19 at 17:32
  • I mean I want no user input on the entire machine to get through. I want to block _all_ user input, everywhere. – Christopher Pisz Jul 11 '19 at 17:48
  • In Unix, you can not block the entire machine. On Windows, I doubt it. Any how, that's not a good idea. No user likes that. – Ripi2 Jul 11 '19 at 17:50
  • Yea, well, it is necessary in order to play back macros without interruption. Otherewise, the click on the button at x,y might not click on a button there, because some other window covered it up, or it moved, or closed. I'd like to get back to the actual question if that's OK. – Christopher Pisz Jul 11 '19 at 18:46

0 Answers0