Native Restart and Logout Dialogs with PyObjC
Recently I wanted to find a friendly way to prompt for logout or restart using the dialog prompts people were already used to. As part of a workflow users had to restart, but the only solutions I found to programmatically accomplish this were to force something like…
sudo shutdown -r nowThat works decently, but can be easily interrupted by a blocking process, is abrupt, and isn’t what people are used to when they restart their Mac. When going to > Restart… everyone’s used to seeing this dialog pop up…
Luckily I found an old pudquick / frogor / Michael Lynn (thanks!) gist on generating native “polite” login window events using PyObjC.
Awesome. He took care of the deep dive API work for me and and I now have working code to generate a logout, restart, or shutdown dialog window on demand. The only problem being it’s written in Python 2. A while back I started shipping relocatable Python 3 interpeter to managed clients to get ready for Python 2 deprecation, and Apple eventually not including it by default with macOS. Running the gist unedited through Python 3.8 with PyObjC installed came back with a lot of errors. After messing around with encoding and a few other functions, I came up with a working Python 3 version. This has been tested with Python 3.8+ and PyObjC 6.1, but should work with most Python 3 versions.
| #!/usr/bin/python3 | |
| # Stolen entirely from Michael Lynn | |
| # https://gist.github.com/pudquick/9683c333e73a82379b8e377eb2e6fc41 | |
| # Edited for Python 3 | |
| import struct | |
| import objc | |
| from Cocoa import NSAppleEventDescriptor | |
| from Foundation import NSBundle | |
| def OSType(s): | |
| # Convert 4 character code into 4 byte integer | |
| return struct.unpack(">I", s.encode())[0] | |
| # Create an opaque pointer type to mask the raw AEDesc pointers we'll throw around | |
| AEDescRef = objc.createOpaquePointerType( | |
| "AEDescRef", b"^{AEDesc=I^^{OpaqueAEDataStorageType}}" | |
| ) | |
| # Load AESendMessage from AE.framework for sending the AppleEvent | |
| AE_bundle = NSBundle.bundleWithIdentifier_("com.apple.AE") | |
| functions = [ | |
| ( | |
| "AESendMessage", | |
| b"i^{AEDesc=I^^{OpaqueAEDataStorageType}}^{AEDesc=I^^{OpaqueAEDataStorageType}}iq", | |
| ), | |
| ] | |
| objc.loadBundleFunctions(AE_bundle, globals(), functions) | |
| # Defined in AEDataModel.h | |
| kAENoReply = 1 | |
| kAENeverInteract = 16 | |
| kAEDefaultTimeout = -1 | |
| kAnyTransactionID = 0 | |
| kAutoGenerateReturnID = -1 | |
| # Defined in AEDataModel.h | |
| typeAppleEvent = OSType("aevt") | |
| typeApplicationBundleID = OSType("bund") | |
| # Defined in AERegistry.h | |
| kAELogOut = OSType("logo") | |
| kAEReallyLogOut = OSType("rlgo") | |
| kAEShowRestartDialog = OSType("rrst") | |
| kAEShowShutdownDialog = OSType("rsdn") | |
| # Build a standalone application descriptor by bundle id | |
| loginwindowDesc = NSAppleEventDescriptor.alloc().initWithDescriptorType_data_( | |
| typeApplicationBundleID, memoryview(b"com.apple.loginwindow") | |
| ) | |
| # Build an event descriptor with our app descriptor as the target and the kAELogOut eventID | |
| event = NSAppleEventDescriptor.appleEventWithEventClass_eventID_targetDescriptor_returnID_transactionID_( | |
| typeAppleEvent, | |
| kAELogOut, | |
| loginwindowDesc, | |
| kAutoGenerateReturnID, | |
| kAnyTransactionID, | |
| ) | |
| eventDesc = event.aeDesc() | |
| # Send a polite logout (returns immediately) | |
| logout = AESendMessage( # noqa: F821 | |
| eventDesc, None, kAENoReply | kAENeverInteract, kAEDefaultTimeout | |
| ) |
The important bits here are the different AERegistry values.
# Defined in AERegistry.h
kAELogOut = OSType("logo")
kAEReallyLogOut = OSType("rlgo")
kAEShowRestartDialog = OSType("rrst")
kAEShowShutdownDialog = OSType("rsdn")
# Build a standalone application descriptor by bundle id
loginwindowDesc = NSAppleEventDescriptor.alloc().initWithDescriptorType_data_(
typeApplicationBundleID, memoryview(b"com.apple.loginwindow")
)
# Build an event descriptor with our app descriptor as the target and the kAELogOut eventID
event = NSAppleEventDescriptor.appleEventWithEventClass_eventID_targetDescriptor_returnID_transactionID_(
typeAppleEvent,
kAELogOut,
loginwindowDesc,
kAutoGenerateReturnID,
kAnyTransactionID,
)On line 59 of my gist change the value kAELogOut to any of the others in that list to get a different dialog and corresponding function. Unsurprisingly, kAELogOut results in a logout dialog.
kAEShowRestartDialog and kAEShowShutdownDialog are self explanatory. Give them a try to see the usual dialog windows you would see from going to > Shutdown… and similar. Be careful when playing with kAEReallyLogOut. Apparently “really log out” means log out immediately with no dialog prompt. In a future post I plan to explain how I used the restart dialog to prompt users when their uptime was over a certain number of days.