moses-palmer / pynput

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

SIGILL on Mac M1 with tkinter #366

Open WillTWinter opened 3 years ago

WillTWinter commented 3 years ago

I'm getting a crash in very specific circumstances. I've reduced it to the following code:

import pynput
import time
import threading
import tkinter as tk
import string

class Window:
    def __init__(self, parent):
        self.text = tk.StringVar()
        self.text.set('Nothing yet')
        tk.Label(parent, textvariable=self.text).grid()

        self.keyboard = pynput.keyboard.Controller()

        # The next line is critical - if uncommented there is no crash
        # self.send_key('x')

        self.thread = threading.Thread(target=self.worker, daemon=True)
        self.thread.start()

    def worker(self):
        characters = list(string.ascii_lowercase)
        i = 0
        while True:
            time.sleep(1)
            key = characters[i % len(characters)]
            i += 1
            self.send_key(key)

    def send_key(self, key):
        self.text.set(f'Pressing "{key}"')
        print(f'Pressing "{key}"')
        self.keyboard.press(key)   # These lines are the source of the crash
        self.keyboard.release(key) #

if __name__ == '__main__':
    root = tk.Tk()
    w = Window(root)
    root.mainloop()

which results in a crash from the self.keyboard.press(key) line in send_key(...). Program output is:

Pressing "a"

Process finished with exit code 132 (interrupted by signal 4: SIGILL)

If I uncomment the marked line in the constructor then I have no problems. If I re-write without a gui then it runs fine. I guess that the issue is somewhere between pynput, tkinter, threading and the new Mac.

I have not had any issues running this code on any other machine, Mac or Linux, though I haven't managed to replicate the exact configuration.

Details of the offending computer:

johnson7788 commented 3 years ago

My issue is, do you know what's this issue. it also is Macbook M1。

    from pynput import keyboard
  File "/Users/admin/git/pynput/lib/pynput/__init__.py", line 40, in <module>
    from . import keyboard
  File "/Users/admin/git/pynput/lib/pynput/keyboard/__init__.py", line 31, in <module>
    backend = backend(__name__)
  File "/Users/admin/git/pynput/lib/pynput/_util/__init__.py", line 76, in backend
    raise ImportError('this platform is not supported: {}'.format(
ImportError: this platform is not supported: dlopen(/Users/admin/virtulenv/py38/lib/python3.8/site-packages/objc/_objc.cpython-38-darwin.so, 2): Symbol not found: _ffi_find_closure_for_code_np
  Referenced from: /Users/admin/virtulenv/py38/lib/python3.8/site-packages/objc/_objc.cpython-38-darwin.so
  Expected in: flat namespace
 in /Users/admin/virtulenv/py38/lib/python3.8/site-packages/objc/_objc.cpython-38-darwin.so

Try one of the following resolutions:

 * Please make sure that you have Python bindings for the system frameworks installed
WillTWinter commented 3 years ago

Sorry - I don't recognise that one. I had python installed from https://www.python.org/downloads/mac-osx/ and pynput installed using pip and didn't run into that issue.

moses-palmer commented 3 years ago

Thank you for your very detailed report.

I know that there are issues on macOS when combining this library with other libraries that want to manage the mainloop, but those mostly relate to listeners.

Unfortunately I do not have access to an M1. Do you know whether your application runs natively on ARM, or is the binary translated?

johnson7788 commented 2 years ago

Thank you very much! Big God, it works now, i install it on ARM Chip, python Python 3.9.2, pynput==1.7.3, Great!

Sick-E commented 2 years ago

Hi, I have problem with start keyboard listener from tkinter button command. When I press button application crash with only message "Process finished with exit code 133 (interrupted by signal 5: SIGTRAP)". When I start only listener or with tkinter but listener is started from main thread, everything works. When I try to run pynput mouse listener this way, there is no problem. I'm using pynput 1.7.6, Python 3.9 on macOS Monterey 12.4 M1 PRO. Thank you for any help.

import tkinter
from tkinter import Tk, Button
from pynput import keyboard

logo = b'iVBORw0KGgoAAAANSUhEUgAAACUAAAAlCAYAAADFniADAAAACXBIWXMAAAsSAAALEgHS3X78AAACOElEQVRYhc2Yy23CQBCG/1jcoYO4ATuc7GPcQdIB3H2hgzgdEMm+Ox2ECmKOvhFXAB1ABURjjdGyLHgX/PqlFcKP3Y+Z3WFmno7HI0zlJv4UQMBjAsAG8MzTrPkzA7ApwvzHdH5tKDfxafE5gIUAoKMDAAJbFmG+aQzKTfyIYcYGMCqtaJ4izLd3Q7GbUgAvD8KIOjBYagzlJn7AZn/UOtf0XYT5XBvKTfxq7+wNFnkFsAMgu6Y6BGvFO3QQFhdXCUocTuxNndjbO7EXyPduDSf2jk7sRYr5IrqneteJvcyJvVS+bkkWmvBRbstlKs3YMydZ0kNpx0CVlm7i2xdQvLHfegACGyKqvoyEG5H6eSPZ/ONE2ZoTkBsjimElFMej1wagZjzuFRlmXrlPGS960DuEPSWbvC+Nyf0Wh4Em/0YeVUCWmg4IiGRbnA8NSfaoK0tx+nNhFdWzI9XFlvShO+3IMBOo02cR5tpB2E38TBUfaU9ppahdymrYUk0osziZPwwIalNF9KxnEFFZBWVcm7WkVRHm+xKKK4shuLA0jph5LvtjKbWryi4ZatcfE05VzQmKfNljXrUSew5nhUMR5nQKvzoG2snGuFaMphzpTaL9L1W9XBHpKuIS/mydW2V7+mC+XSc67YGqEyPXfSdxnd+WK/8oZbrWGqptBXHJlBr2pG6pNpMwaZrNeQ/cC0f7LarrTRlBCXBUBtEgC9YBUpOMTnTKIUdLd/U8BUDK71Xp9FbHIkoB+AdGxhvJUKsuTwAAAABJRU5ErkJggg=='

root = Tk()
root.title("Test window")
root.geometry("400x200")
core_logo_icon = tkinter.PhotoImage(data=logo)

def start_listener():
    def on_press(key):
        print("Key ", key, " pressed")

    def on_release(key):
        print("Key ", key, " released")

    keyboard_listener = keyboard.Listener(on_press=on_press, on_release=on_release)
    keyboard_listener.start()
    print("Keyboard listener ON")

button = Button(root, command=start_listener, image=core_logo_icon, borderwidth=0)
button.pack()
root.mainloop()

Edit: On Windows 11 everything works fine. When I run app from terminal, err msg is "zsh: trace trap path/to/py/script"...

toddthegeek commented 1 year ago

I have tried the code posted by WillTWinter (OP), and I also receive the same error. It is not an M1 issue. I am on an Intel CPU testing via KVM/QEMU.

My setup is macOS Catalina (Version 10.15.7) Processor: 2.11 GHz Quad-Core Intel Xeon Python 3.11.1 (v3.11.1:a7a450f87a CLang 13.0.0 on darwin) pynput 1.7.6

Here is the head of the error message received (also attached as a file)

Process:                              Python [1265]
Path:                                    /Library/Frameworks/Python.framework/Versions/3.11/Resources/Python.app/Contents/MacOS/Python
Identifier:                        org.python.python
Version:                              3.11.1 (3.11.1)
Code Type:                          X86-64 (Native)
Parent Process:                Python [1167]
Responsible:                      Python [1167]
User ID:                              501

Date/Time:                          2022-12-23 12:41:53.453 -0600
OS Version:                        Mac OS X 10.15.7 (19H15)
Report Version:                12
Anonymous UUID:                8F3C4275-7530-4603-A4FA-C2D5E4640406

Time Awake Since Boot: 3100 seconds

System Integrity Protection: enabled

Crashed Thread:                8

Exception Type:                EXC_BAD_INSTRUCTION (SIGILL)
Exception Codes:              0x0000000000000001, 0x0000000000000000
Exception Note:                EXC_CORPSE_NOTIFY

Termination Signal:        Illegal instruction: 4
Termination Reason:        Namespace SIGNAL, Code 0x4
Terminating Process:      exc handler [1265]

Application Specific Information:
BUG IN CLIENT OF LIBDISPATCH: Assertion failed: Block was expected to execute on queue [com.apple.main-thread]

test.log.txt

I am having same error in my own program.

I, also, like OP believe this error is related to a threading issue. My first thought is the pynput controller thread is operating from within the tk inter thread and macOS does not like that. Just a though, I have not much programming experience.

toddthegeek commented 1 year ago

I have modified the original code by OP. It is running without throwing an error.

#!/usr/bin/env python3
#!python3

import pynput
import time
import tkinter as tk
import string

def repeat_alphabet(i=0, characters=list(string.ascii_lowercase)):
    key = characters[i % len(characters)]
    i += 1
    label.set(f'Pressing "{key}"')
    print(f'Pressing "{key}"')
    keyboard.press(key)
    keyboard.release(key)
    root.after(1000, repeat_alphabet, i)

if __name__ == '__main__':
    keyboard = pynput.keyboard.Controller()
    root = tk.Tk()
    label = tk.StringVar()
    label.set('Nothing yet')
    tk.Label(root, textvariable=label,).pack()
    repeat_alphabet()
    root.mainloop()
toddthegeek commented 1 year ago

So a similar approach solved my problem as well. I've simplified my code as much as possible to demonstrate the same functionality.

How to use it. Run the script. Open a text editor. Type letters. Then type the special key "F3" and it should type into the text editor.

Script #1 does not work and crashes when I press the special key "F3". Script #2 does work.

Can anyone tell me the reason?

script #1

#!/usr/bin/env python3
#!python3

import pynput
import tkinter as tk

thiskey = 'f3'

def on_press(presskey):
    if presskey == pynput.keyboard.Key.esc:
        print('bye!')
        return False  # stop listener
    if presskey == pynput.keyboard.Key[thiskey]:
        controller.type(f'{thiskey} found and typing!!!')
        print(f'{thiskey} found')
        label.set(f'{presskey=}')
    else:
        print(f'{presskey=}', type(presskey), sep=':')
        label.set(f'{presskey=}')
    return True

def do_basically_nothing():
    root.after(100, do_basically_nothing)

controller = pynput.keyboard.Controller()
listener = pynput.keyboard.Listener(on_press=on_press)
listener.start()  # start to listen on a separate thread
root = tk.Tk()
label = tk.StringVar()
label.set('Nothing yet')
tk.Label(root, textvariable=label,).pack()
do_basically_nothing()
root.mainloop()

script #2

#!/usr/bin/env python3
#!python3

import pynput
import tkinter as tk

thiskey = 'f3'
presskey = ''

def on_press(key):
    global presskey
    presskey = key
    return True

def do_basically_nothing():
    if presskey == pynput.keyboard.Key.esc:
        print('bye!')
        return False  # stop listener
    if presskey == pynput.keyboard.Key[thiskey]:
        controller.type(f'{thiskey} found and typing!!!')
        print(f'{thiskey} found')
        label.set(f'{presskey=}')
    else:
        print(f'{presskey=}', type(presskey), sep=':')
        label.set(f'{presskey=}')
    root.after(100, do_basically_nothing)

controller = pynput.keyboard.Controller()
listener = pynput.keyboard.Listener(on_press=on_press)
listener.start()  # start to listen on a separate thread
root = tk.Tk()
label = tk.StringVar()
label.set('Nothing yet')
tk.Label(root, textvariable=label,).pack()
do_basically_nothing()
root.mainloop()
toddthegeek commented 1 year ago

I figured out my own question. So much code and learning in one day.

From how I understand it, tkinter (GUI) and the pynput keyboard controller both run on the main python thread. The listener runs on a separate thread. I was calling the controller from the listener thread. That is why it was crashing. (Oddly enough, the wrong way worked in both Windows and Linux, but not in macOS. I wonder why it worked on those, it really shouldn't. )

I fixed my mistake by setting global variables from my listener thread to tell me if a key is pressed or not. I then have a tkinter function which checks if the keys are pressed, does what it needs to, and then sets the key to not pressed.

WillTWinter I believe this to also be your issue. It looks like you are calling tkinter in your Window class, and in the same class you are starting another thread.

PauloMesquitaSP commented 6 months ago

@toddthegeek Can you be more specific on how did you do this? I'm having the same problem but with GTK and by my tests I really think it has to do with threads.