pyobjc icon indicating copy to clipboard operation
pyobjc copied to clipboard

Python crashes when sending a User Notifcation, zsh: illegal hardware instruction

Open leifadev opened this issue 2 years ago • 3 comments

Describe the bug I send a notification with the User Notifications framework (UNNotification* superclass) and python crashes, looking like this: Screen Shot 2022-07-14 at 7 31 24 AM

It does not crash Pycharm or interrupt the notification from sending as the notification will appear, however the same pop-up with this stack trace always will show up. Also, I also get a zsh error thrown below as well:

zsh: illegal hardware instruction  python3 unnotif.py

I think this has something to do with this but I am not sure where it comes from, I can not find any posts that explain what it is exactly I now docs for now either. I am rather confused after that on what to do, I have not yet compiled it and tried this yet though.

Platform information

  • Python Version: 3.9.2
  • PyCharm venv: /Users/leif/PycharmProjects/shoutout/venv/bin/python3
    • Python installed with Homebrew, however in the HB folder there is no python binary but its in my /Library/Frameworks directory...
  • macOS 10.15.7 (Catalina)
  • MacBook Pro 2017 Intel

To Reproduce This is the code: https://pastebin.com/m2t0uDKr I used Pycharm 2022 and 2021, ran the code with packages installed with PyObj-C 8.5 in the terminal with the venv python binary with the command python3 unnotif.py.

Expected behavior What should be happening is that there is no zsh: illegal hardware exception thrown, and thus probably as well Python not crashing bringing up the popup error every time.

Additional context Nothing too much else

leifadev avatar Jul 20 '22 15:07 leifadev

The code:

from uuid import uuid4
import UserNotifications as UN
import Foundation

DEFAULT_AUTH_OPTIIONS = UN.UNAuthorizationOptions(UN.UNAuthorizationOptionBadge)

class NotificationScheduler:
    center = UN.UNUserNotificationCenter.currentNotificationCenter()
    
    def __init__(self): # Init objects
        self.granted = None
        self.settings = None
        self.timeInterval = 1 # In minutes
        self.repeatNotif = True # Repeat notification? Boolean


        self.center.getNotificationSettingsWithCompletionHandler_(self._settingsHandler)

    def _haveAuthorization(self, block=False):
        if block:
            while self.granted is None:
                pass

        return bool(self.granted)

    def _settingsHandler(self, settings):
        self.settings = settings

        # if settings.authorizationStatus() == UNAuthorizationStatusDenied:
        self.center.requestAuthorizationWithOptions_completionHandler_(DEFAULT_AUTH_OPTIIONS,
                                                                       self._authorizationRequestHandler)

    def _authorizationRequestHandler(self, granted, error):
        self.granted = granted
        print(f"Completion handler granted: {granted}")
        if error:
            print(error)

    def _notificationRequestHandler(self, error):
        print("_notificationRequestHandler")
        if error:
            print(error)

    def addNotificationRequest(self, title, body):
        if not self._haveAuthorization(block=True):
            print("Error: Missing authorization to add notification request.")
            return None

        # Trigger repeats every minute (may not be using this though)
        trigger = UN.UNTimeIntervalNotificationTrigger.triggerWithTimeInterval_repeats_(60, True)

        # Making notification content with instance of UNMutableNotificationContent class
        content = UN.UNMutableNotificationContent.alloc().init()
        content.setTitle_(title)
        content.setSubtitle_("Reminder")
        content.setBody_(body)
        content.setSound_(UN.UNNotificationSound.defaultSound())
        content.setCategoryIdentifier_("Shoutout")

        # Attachments (logo)
        # fileURL = Foundation.NSURL.fileURLWithPath_("/Users/leif/PycharmProjects/shoutout/images/shoutout_logo.png")
        # attachments = UN.UNNotificationAttachment.attachmentWithIdentifier_URL_options_error_\
        #     ("Shoutout_image", fileURL, {}, None)
        # content.setAttachments_([attachments])

        # Actions for notification
        # action_open = UN.UNNotificationAction.actionWithIdentifier_title_options_("open", "Open", [])
        # category = UN.UNNotificationCategory.categoryWithIdentifier_actions_intentIdentifiers_options_("Shoutout", [action_open], [])
        # self.center.setNotificationCategories_([category])


        # Make a random identifier
        identifier = str(uuid4())

        # Form NotificationRequest object to be sent to current notification center
        request = UN.UNNotificationRequest.requestWithIdentifier_content_trigger_(identifier, content, None)
        self.center.addNotificationRequest_withCompletionHandler_(request, self._notificationRequestHandler)


if __name__ == "__main__":
    notificationScheduler = NotificationScheduler()
    notificationScheduler.addNotificationRequest("Shoutout!", "You have a word of the day to check!")


# Time Interval Class
# https://developer.apple.com/documentation/usernotifications/untimeintervalnotificationtrigger?language=objc

ronaldoussoren avatar Jul 22 '22 06:07 ronaldoussoren

This doesn't seem to be a bug in PyObjC itself, but in the interaction between PyObjC and Cocoa during interpreter shutdown. That's something I cannot (easily) fix.

What appears to happen:

  • You schedule a notification (addNotificationRequest:withCompletionHandler:)
  • The script exits before this is finished, shutting down the interpreter
  • Cocoa calls the completion handler on a secondary thread, but the interpreter is already gone
  • BOOM!

APIs like this generally require a running runloop. One way to avoid the crash is to run the Cocoa runloop for a while (e.g. call NSRunLoop.currentRunLoop.run() and arrange to stop the runloop when you are done).

ronaldoussoren avatar Jul 22 '22 06:07 ronaldoussoren

Thank you man! I put this:

if __name__ == "__main__":
    notificationScheduler = NotificationScheduler()
    notificationScheduler.addNotificationRequest("Shoutout!", "Reminder", "You have a word of the day to check!")

    import Cocoa
    loop = Cocoa.NSRunLoop.currentRunLoop() # Returns the run loop for the current thread (once?)

Is this ok? The run method says it does a permanent loop, so I just added NSRunLoop.currentRunLoop() and it works fine. It says it,

Returns the run loop for the current thread. Which I think is just 1 time, and I don't think I need to close anything then I think? I don't know how to do that unless I use a untilDate method and put an NSDate object in there.

leifadev avatar Jul 22 '22 19:07 leifadev

You also have to arrange for the loop to run for a while, using the runMode_beforeDate_ method. currentRunLoop just returns the runloop for the current thread.

See also: https://developer.apple.com/documentation/foundation/nsrunloop/1411525-runmode

ronaldoussoren avatar Oct 21 '22 11:10 ronaldoussoren