vpelletier / python-libusb1

Python ctype-based wrapper around libusb1
GNU Lesser General Public License v2.1
174 stars 67 forks source link

Needs a binding for libusb_interrupt_event_handler #90

Closed whitequark closed 11 months ago

whitequark commented 11 months ago

The recommended way to use multithreading with libusb1 is to use a poller thread. However, this results in a hang when exit() is called (https://github.com/GlasgowEmbedded/glasgow/issues/413):

import threading
import usb1

class _PollerThread(threading.Thread):
    def __init__(self, context):
        super().__init__()
        self.done    = False
        self.context = context

    def run(self):
        while not self.done:
            self.context.handleEvents()

usb_context = usb1.USBContext()
usb_poller = _PollerThread(usb_context)
usb_poller.start()
usb_handle = usb_context.openByVendorIDAndProductID(0x20b7, 0x9db1) # any device works

exit()

As far as I can tell this is not feasible to fix (without adding a non-insignificant delay on shutdown) with python-libusb1 because it does not include a binding for libusb_interrupt_event_handler(), included since libusb 1.0.21.

Could you please add this function and make a release for the Glasgow Interface Explorer project?

whitequark commented 11 months ago

After implementing the functionality in the straightforward way (PR coming in a bit) I discovered that you also have to use the internal threading._register_atexit function to ensure the application always actually exits on exit(). This is illustrated using the 10 testcases below, folded into a single snippet of code:

# behavior = {} # hangs
# behavior = {"normal-atexit"} # hangs
# behavior = {"threading-atexit"} # EXITS
# behavior = {"normal-atexit", "interrupt-handler"} # hangs
# behavior = {"threading-atexit", "interrupt-handler"} # EXITS
# behavior = {"open-device"} # hangs
# behavior = {"open-device", "normal-atexit"} # hangs
# behavior = {"open-device", "threading-atexit"} # hangs
# behavior = {"open-device", "normal-atexit", "interrupt-handler"} # hangs
# behavior = {"open-device", "threading-atexit", "interrupt-handler"} # EXITS

import threading
import atexit
import usb1

class _PollerThread(threading.Thread):
    def __init__(self, context):
        super().__init__()
        self.done    = False
        self.context = context

    def run(self):
        if "normal-atexit" in behavior:
            atexit.register(self.stop)
        if "threading-atexit" in behavior:
            threading._register_atexit(self.stop)
        print("before handleEvents loop")
        while not self.done:
            print("before handleEvents")
            self.context.handleEvents()
            print("after handleEvents")
        print("after handleEvents loop")

    def stop(self):
        print("setting done")
        self.done = True
        if "interrupt-handler" in behavior:
            print("interrupting event handler")
            self.context.interruptEventHandler()

usb_context = usb1.USBContext()
usb_poller = _PollerThread(usb_context)
usb_poller.start()
if "open-device":
    print("opening device")
    usb_handle = usb_context.openByVendorIDAndProductID(0x20b7, 0x9db1) # any device works

print("exiting")
exit()

So without using threading._register_atexit the application always hangs, while using it if there is an open device (which is GC'd during the exit process in this particular snippet, causing a libusb event that coincidentally wakes up the process) lets things work without interruptEventHandler, and interruptEventHandler, as expected, makes things work regardless.

whitequark commented 11 months ago

@vpelletier Thanks a lot! Very quick as usual. May I have a release?

vpelletier commented 11 months ago

@vpelletier Thanks a lot! Very quick as usual.

Thank you for the clean patch :)

May I have a release?

Yep, working on it.

vpelletier commented 11 months ago

Done as 3.1.0 .

whitequark commented 11 months ago

Thanks! (I think you've made a release with the changelog entry as "unreleased" which is a little amusing to me.)