moses-palmer / pynput

Sends virtual input commands
GNU Lesser General Public License v3.0
1.79k stars 248 forks source link

Windows not Releasing Global HotKeys when Callback contains a Sleep #322

Closed basharkey closed 3 years ago

basharkey commented 3 years ago

On Windows when a global hotkey triggers a callback that contains a sleep greater than 1 second the keys used to trigger the global hotkey will not be released after the callback has finished executing. This means that pynput still thinks that certain keys are pressed even through they have been released.

Notice when run on Linux the hotkeys, keys ('\<shift>+a') are released after the callbacks execution. linux_pynput

However when ran on Windows the hotkeys, keys ('\<shift>+a') are never released. windows_pynput

Code used to test:

from pynput import keyboard
import time

def on_hotkey_a():
    print('Hot key A pressed!')
    time.sleep(1)

def on_hotkey_b():
    print('Hot key B pressed!')

HOTKEYS = [
    keyboard.HotKey(keyboard.HotKey.parse('<shift>+a'), on_hotkey_a),
    keyboard.HotKey(keyboard.HotKey.parse('<shift>+b'), on_hotkey_b)]

def on_press(key):
    print(f"pressed: {key}")
    for hotkey in HOTKEYS:
        hotkey.press(l.canonical(key))
    # Handle pressed keys

def on_release(key):
    print(f"released: {key}")
    for hotkey in HOTKEYS:
        hotkey.release(l.canonical(key))
    # Handle released keys

with keyboard.Listener(
        on_press=on_press,
        on_release=on_release) as l:
    l.join()
basharkey commented 3 years ago

Wrote a quick monkey patch to solve the issue but I don't know the full implications of this fix.

import platform

def windows_hotkey_fix():
    from pynput.keyboard import HotKey

    def press_fixed(self, key):
        """Updates the hotkey state for a pressed key.

        If the key is not currently pressed, but is the last key for the full
        combination, the activation callback will be invoked.

        Please note that the callback will only be invoked once.

        :param key: The key being pressed.
        :type key: Key or KeyCode
        """
        if key in self._keys and key not in self._state:
            self._state.add(key)
            if self._state == self._keys:
                # reset state
                self._state = set()
                self._on_activate()

    keyboard.HotKey.press = press_fixed

if platform.system() == 'Windows':
    windows_hotkey_fix()
moses-palmer commented 3 years ago

Thank you for your report and workaround.

As stated in the documentation, the callbacks must not block for long, as that will lead to random issues, such as this. I am reluctant to add a workaround for this specific issue, as many more are certain to be found elsewhere. Perhaps the documentation should be updated with stronger wording?

basharkey commented 3 years ago

For anyone interested I came up with a more elegant solution using queues and threads as recommended by the documentation for callbacks that block for long amounts of time.

from pynput import keyboard
import time
from queue import Queue
from threading import Thread

def on_hotkey_a():
    print('Hot key A pressed!')
    time.sleep(2)
    print('done')

def on_hotkey_b():
    print('Hot key B pressed!')

def on_press(key):
    print(f"pressed: {key}")
    for hotkey in HOTKEYS:
        hotkey.press(l.canonical(key))
    # Handle pressed keys

def on_release(key):
    print(f"released: {key}")
    for hotkey in HOTKEYS:
        hotkey.release(l.canonical(key))
    # Handle released keys

def worker(q):
    while True:
        func = q.get()
        func()
        q.task_done()
        print('task done')

q = Queue(maxsize=0)
num_threads = 1

a_callback = lambda: q.put(on_hotkey_a)
b_callback = lambda: q.put(on_hotkey_b)

HOTKEYS = [
    keyboard.HotKey(keyboard.HotKey.parse('<shift>+a'), a_callback),
    keyboard.HotKey(keyboard.HotKey.parse('<shift>+b'), b_callback)]

for i in range(num_threads):
  worker = Thread(target=worker, args=(q,))
  worker.setDaemon(True)
  worker.start()

with keyboard.Listener(
        on_press=on_press,
        on_release=on_release) as l:
    l.join()