ronaldoussoren / pyobjc

The Python <-> Objective-C Bridge with bindings for macOS frameworks
https://pyobjc.readthedocs.io
537 stars 47 forks source link

SIGTRAP when using threading events to wait for completion handler #609

Open torablien opened 3 months ago

torablien commented 3 months ago

I'm new to pyobjc so this definitely could be a beginner mistake.

I'm trying to figure out the best way to wait for completion handlers to finish and encapsulate that into a simpler synchronous-like function. I've been testing with threading.Event and although it generally works, I've noticed that if the program exits immediately after, I see a SIGTRAP (zsh: trace trap ...). Is there a proper/better way to do this?

import threading

def get_shareable_applications():
    from ScreenCaptureKit import SCShareableContent  # type: ignore

    shareable_applications = []

    completion_event = threading.Event()

    def shareable_content_completion_handler(shareable_content, error):
        try:
            nonlocal shareable_applications
            shareable_applications = shareable_content.applications()
            # Potentially more stuff here including calls to other methods with completion handlers
        finally:
            completion_event.set()

    SCShareableContent.getShareableContentWithCompletionHandler_(
        shareable_content_completion_handler
    )

    completion_event.wait()

    return shareable_applications

def main():
    get_shareable_applications()

main()

Running Python 3.12 on M1 Pro 14.1.2

Thanks!

ronaldoussoren commented 3 months ago

Completion handlers in general only get called when there's an active Cocoa event loop (the exception being when you can specify a dispatch queue, those often send work to a background thread and call the completion handler on a background thread as well).

That's not the case here though. The completion handler gets called in a background thread and there appears to be a race condition between cleaning up on the background thread (which is a daemon thread as far as the CPython runtime is concerned) and the main thread exiting.

A quick workaround for this is to add a short sleep after waiting for the completion_event. On my system time.sleep(0.1) worked reliably, but YMMV. With some luck the sleep isn't necessary when you integrate this in a larger code base.

I'm keeping the issue open because I want to do further research to see if I can avoid the crash (or have to file an issue with the cpython project).

torablien commented 2 months ago

Thanks @ronaldoussoren - are there any example implementations or recommendations on how to work with completion handlers?

I tried some simple implementations with AppHelper.runConsoleEventLoop(), Foundation.CFRunLoopRun(), but couldn't get them to work well/robustly (e.g. terminating program, hanging if there is an error). Python's threading.Event somewhat works but is also running into issues (e.g. sometimes some completion handlers seem to never get called and the Event times out, but something lingers that slowly bring my machine to a halt). I get the sense that this is not the best way to do this - if you have any recommendations, that would be greatly appreciated!

ronaldoussoren commented 2 months ago

Thanks @ronaldoussoren - are there any example implementations or recommendations on how to work with completion handlers?

I tried some simple implementations with AppHelper.runConsoleEventLoop(), Foundation.CFRunLoopRun(), but couldn't get them to work well/robustly (e.g. terminating program, hanging if there is an error). Python's threading.Event somewhat works but is also running into issues (e.g. sometimes some completion handlers seem to never get called and the Event times out, but something lingers that slowly bring my machine to a halt). I get the sense that this is not the best way to do this - if you have any recommendations, that would be greatly appreciated!

I don't have a good solution at this moment, and is something I want to research over the summer anyway because issues like this will crop up more given Apple's switch to Swift and the concurrency model of Swift (which is spelled as "async/await" but uses pervasive threading)

I hope to have some time over the summer to finish some work on asyncio integration for PyObjC, including adding async methods for all Objective-C selectors with a completion handler argument. This should help hide most of the complexity in shared code, and should also result in cleaner code in scripts.

That should end up with code like this for your problem:

import asyncio
from ScreenCaptureKit import SCShareableContent

async def get_shareable_applications():
     return await SCShareableContent.getShareableContent()

asyncio.run(get_shareable_applications())

I still have to work out a good design though, and this likely requires using a PyObjC implementation of an asyncio runloop in general (but not necessarily in this use case).

torablien commented 2 months ago

Thanks, appreciate it. If you come across a good workaround, I'd be interested in trying it. An API for these kinds of situations (e.g. async methods) would be great.