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 now
That 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.