Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MacOS crash when starting pynput keyboard listener after qt6 #511

Open
perroboc opened this issue Oct 24, 2022 · 6 comments · May be fixed by #512
Open

MacOS crash when starting pynput keyboard listener after qt6 #511

perroboc opened this issue Oct 24, 2022 · 6 comments · May be fixed by #512

Comments

@perroboc
Copy link

perroboc commented Oct 24, 2022

Description
If I create and start a listener AFTER launching a qt6 app (pyside6), the application crashes.

Platform and pynput version
macOS Monterey (12.6), M1 Pro. Pynput 1.7.6, Pyside6 6.4.0, Python 3.10. I'm also using a Latin American keyboard layout.

To Reproduce
https://github.com/alvaromunoz/pynput-macos-issues/blob/ba17e4b83f1eb239454d73ba96adf7f5fe71673b/nested_crash.py

import sys

from PySide6 import (
    QtWidgets,
)

from pynput import keyboard

class MyListener():

    def __init__(self, label: QtWidgets.QLabel):

        def on_press(key):
            label.setText("You pressed {0}".format(key))

        def on_release(key):
            label.setText("You released {0}".format(key))

        self.listener = keyboard.Listener(
            on_press=on_press,
            on_release=on_release)
        
    def start(self):
        self.listener.start()
        self.listener.wait()
        print("listener has started? {0}".format(self.listener.running))

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Listener Demo 2")

        central_widget = QtWidgets.QWidget()

        layout = QtWidgets.QVBoxLayout()
        central_widget.setLayout(layout)

        label_description = QtWidgets.QLabel("Press the button to crash")
        layout.addWidget(label_description)

        label = QtWidgets.QLabel("Press any key")
        label.setEnabled(False)
        layout.addWidget(label)

        button = QtWidgets.QPushButton("Start listener!")
        button.pressed.connect(self.start_listener)
        layout.addWidget(button)
        
        self.listener = MyListener(label)

        self.setCentralWidget(central_widget)

    def start_listener(self):
        print("beep")
        self.listener.start()
        print("boop")

app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

OUTPUT:

beep
zsh: trace trap
@perroboc perroboc changed the title MacOS crash when starting pynput after qt6 MacOS crash when starting pynput keyboard listener after qt6 Oct 25, 2022
@perroboc
Copy link
Author

FYI: this happens with tkinter and pyqt6, too:

tkinter:

from tkinter import *

from pynput import keyboard

class MyListener():

    def __init__(self):

        def on_press(key):
            print("You pressed {0}".format(key))

        def on_release(key):
            print("You released {0}".format(key))

        self.listener = keyboard.Listener(
            on_press=on_press,
            on_release=on_release)
        
    def start(self):
        self.listener.start()
        self.listener.wait()
        print("listener has started? {0}".format(self.listener.running))

listener = MyListener()

root = Tk()  # create parent window

def start_pynput():
    print("beep")
    listener.start()
    print("boop")

# use Button and Label widgets to create a simple TV remote
button = Button(root, text="Start Pynput", command=start_pynput)
button.pack()

root.mainloop()

pyqt6

import sys

from PyQt6 import (
    QtWidgets,
)

from pynput import keyboard

class MyListener():

    def __init__(self, label: QtWidgets.QLabel):

        def on_press(key):
            label.setText("You pressed {0}".format(key))

        def on_release(key):
            label.setText("You released {0}".format(key))

        self.listener = keyboard.Listener(
            on_press=on_press,
            on_release=on_release)
        
    def start(self):
        self.listener.start()
        self.listener.wait()
        print("listener has started? {0}".format(self.listener.running))

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Listener Demo 2")

        central_widget = QtWidgets.QWidget()

        layout = QtWidgets.QVBoxLayout()
        central_widget.setLayout(layout)

        label_description = QtWidgets.QLabel("Press the button to crash")
        layout.addWidget(label_description)

        label = QtWidgets.QLabel("Press any key")
        label.setEnabled(False)
        layout.addWidget(label)

        button = QtWidgets.QPushButton("Start listener!")
        button.pressed.connect(self.start_listener)
        layout.addWidget(button)
        
        self.listener = MyListener(label)

        self.setCentralWidget(central_widget)

    def start_listener(self):
        print("beep")
        self.listener.start()
        print("boop")

app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

@perroboc perroboc linked a pull request Oct 31, 2022 that will close this issue
@ba361006
Copy link

Same here, pynput works fine pyperclip, PySide6 at the first place, but it shows error message as follow after pressing the button.

Error message: [1] 31161 trace trap /<path_to_current_folder/.venv/bin/python

working environment:

  • macOS Monterey 12.6, M1 pro
  • Python 3.9.13
  • PySide6 6.4.0.1
  • pyperclip 1.8.2
  • pynput 1.7.6

here's the code

# -*- coding: utf-8 -*-
import random
import string
import time

import pyperclip
from pynput import keyboard
from PySide6 import QtCore
from PySide6 import QtWidgets

class Main(QtWidgets.QMainWindow):
    signal = QtCore.Signal()

    def __init__(self):
        super().__init__()

    def build(self):
        self.signal.connect(self.get_text_from_clip)
        self.button = QtWidgets.QPushButton()
        self.button.clicked.connect(self.button_pressed)
        self.keyboard_detect_start()
        self.setCentralWidget(self.button)
        self.show()

    def keyboard_detect_start(self, key="<ctrl>+q"):
        def for_canonical(hotkey_event):
            return lambda key: hotkey_event(self.listener.canonical(key))

        hotkey = keyboard.HotKey(keyboard.HotKey.parse(key), self.signal.emit)
        self.listener = keyboard.Listener(
            on_press=for_canonical(hotkey.press),
            on_release=for_canonical(hotkey.release),
        )
        self.listener.start()

    def get_text_from_clip(self):
        controller = keyboard.Controller()
        controller.press(keyboard.Key.cmd)
        controller.press("c")
        controller.release("c")
        controller.release(keyboard.Key.cmd)
        time.sleep(0.05)
        print(pyperclip.paste().strip())

    def button_pressed(self):
        self.listener.stop()
        self.listener.join()
        time.sleep(0.5)
        new_key = random.choices(string.ascii_lowercase)[0]
        print("new_key: ", new_key)
        self.keyboard_detect_start(key=f"<ctrl>+{new_key}")


if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    main = Main()
    main.build()
    app.exec()

@perroboc
Copy link
Author

@ba361006 could you please check if #512 fixes your issue?

@ba361006
Copy link

ba361006 commented Nov 20, 2022

@AlvaroMunoz

Yes, #512 does fix this for me.
It would be appreciated that if #512 can be merged :)

[Edited] 2022/11/22
With applying #512 , the following fail case still raises [1] 93758 trace trap <path_to_project>/.venv/bin/python error, but somehow it works by invoking print_text_from_clipboard via signal

So far I know is that the fail case stop at controller = keyboard.Controller() and raise the error mentioned above.
Firstly I thought maybe Mac will limit only one thread to have control of keyboard event, so I instantiate worker and connect the signal under Main which should be at the same scope with the app, and eventually it works.

But if I simply change the way of invoking print_text_from_clipboard it fails, which is the only difference between fail and work case.

  • Fail case
# -*- coding: utf-8 -*-
import time
import platform
import pyperclip
from pynput import keyboard
from PySide6 import QtCore
from PySide6 import QtWidgets

class Worker(QtCore.QObject):
    def __init__(self):
        super().__init__()
    
    def run_method(self):
        activate_key = "<ctrl>+q"
        def for_canonical(hotkey_event):
            return lambda key: hotkey_event(self.listener.canonical(key))
        hotkey = keyboard.HotKey(keyboard.HotKey.parse(activate_key), self.print_text_from_clipboard)
        self.listener = keyboard.Listener(
            on_press=for_canonical(hotkey.press),
            on_release=for_canonical(hotkey.release),
        )
        self.listener.start()
    
    def print_text_from_clipboard(self):
        # delay is needed before getting str from clip
        if platform.system() == "Windows":
            modifier = keyboard.Key.ctrl
        elif platform.system() == "Darwin":
            modifier = keyboard.Key.cmd
        controller = keyboard.Controller()
        controller.press(modifier)
        controller.press("c")
        controller.release("c")
        controller.release(modifier)
        time.sleep(0.05)
        print("text: ", pyperclip.paste().strip())

class Main(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

    def build(self):
        self.worker = Worker()
        
        self.button = QtWidgets.QPushButton()
        self.button.clicked.connect(self.button_pressed)
        self.setCentralWidget(self.button)
        self.show()
        
    def button_pressed(self):
        self.worker.run_method()
        
if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    main = Main()
    main.build()
    app.exec()
  • Work case
# -*- coding: utf-8 -*-
import time
import platform
import pyperclip
from pynput import keyboard
from PySide6 import QtCore
from PySide6 import QtWidgets

class Worker(QtCore.QObject):
   signal = QtCore.Signal()
   def __init__(self):
       super().__init__()
   
   def run_method(self):
       activate_key = "<ctrl>+q"
       def for_canonical(hotkey_event):
           return lambda key: hotkey_event(self.listener.canonical(key))
       
       hotkey = keyboard.HotKey(keyboard.HotKey.parse(activate_key), self.signal.emit)
       self.listener = keyboard.Listener(
           on_press=for_canonical(hotkey.press),
           on_release=for_canonical(hotkey.release),
       )
       self.listener.start()

   def print_text_from_clipboard(self):
       # delay is needed before getting str from clip
       if platform.system() == "Windows":
           modifier = keyboard.Key.ctrl
       elif platform.system() == "Darwin":
           modifier = keyboard.Key.cmd
       controller = keyboard.Controller()
       controller.press(modifier)
       controller.press("c")
       controller.release("c")
       controller.release(modifier)
       time.sleep(0.05)
       print("text: ", pyperclip.paste().strip())

class Main(QtWidgets.QMainWindow):
   def __init__(self):
       super().__init__()

   def build(self):
       self.worker = Worker()
       self.worker.signal.connect(self.worker.print_text_from_clipboard)
       
       self.button = QtWidgets.QPushButton()
       self.button.clicked.connect(self.button_pressed)
       self.setCentralWidget(self.button)
       self.show()
       
   def button_pressed(self):
       self.worker.run_method()
       
if __name__ == "__main__":
   app = QtWidgets.QApplication([])
   main = Main()
   main.build()
   app.exec()

@ba361006
Copy link

ba361006 commented Nov 22, 2022

Just found something interesting.

Without applying changes in #512, the following code works.
It seems that you can't invoking keyboard.Listener via Signal or clicked.connect.

It reminds me that PyQt5 have some tricky stuff with QThread related to scope problem, which means where you instantiate the QThread or invoking moveToThread to your customised QObject class matters, but I can't recall the detail.

Does this code work for you? @AlvaroMunoz

# -*- coding: utf-8 -*-
from pynput import keyboard
from PySide6 import QtWidgets


class Main(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

    def build(self):
        def for_canonical(hotkey_event):
            return lambda key: hotkey_event(self.listener.canonical(key))
        hotkey = keyboard.HotKey(keyboard.HotKey.parse("<ctrl>+q"), lambda: print("keyboard detected"))
        self.listener = keyboard.Listener(
            on_press=for_canonical(hotkey.press),
            on_release=for_canonical(hotkey.release),
        )
        self.listener.start()
        self.show()


if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    main = Main()
    main.build()
    app.exec()

@9783e6
Copy link

9783e6 commented Aug 28, 2023

Same issue, MacBook Air M2 on MacOs Sonoma 14.0 Beta

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants