Programmatically logout a user in Swift
Tip12 August 2021
As well as restarting, shutting down and putting the computer to sleep.
Motivation
I have seen many Mac admins asking for a way to log out the user in Swift. As I had to do that some time ago, I thought it would be useful to share that here. I mostly read the code from the Apple doc and from SplashBuddy . Meanwhile, I’ll try to give some explanations along the way. The final code can be found on this gist.
TLDR
You can copy and paste the code in the gist inside a file like EventService.swift and call the function EventService.send(event:)
.
Also, you’ll have to add the following key/value to the entitlements.
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.loginwindow</string>
</array>
Sending events
The Apple documentation explains that to programmatically shutdown, restart, put to sleep or logout a machine is possible by sending an Apple event. From what I understand, macOS allows to send and receive Apple events. I interpret that a bit like distributed notifications, maybe more powerful but also more complex to use.
From an application, it’s possible to send an event to the loginwindow
process to ask to shutdown, restart, put to sleep or logout the computer.
Of course when C is involved, it’s not that easy. So in this article, we’ll build an EventService
with a single static function send
that will take an event type as a parameter. For instance
EventService.send(event: .restartComputer)
Models
From the function above, we can extract two types:
-
EventService
which is anenum
with no cases. Why using anenum
? When implementing features that do not have a state, and are merely a collection of functions, I think it’s better to be clear about it. Using anenum
with no cases in Swift is common for purpose like namespacing because anenum
with no cases cannot be instantiated. So theEventService
is really only a name to access to the Apple event services through functions. If we were to use astruct
, it could be instantiated unless theinit()
is private which requires to mark it like so.
-
AppleEventType
which will gather the 4 possibles event types we want to send: shutdown, restart, put to sleep and logout.
So let’s start with that.
enum EventService {}
// MARK: - Logic
// logic will come here
// MARK: - Models
extension EventService {
enum AppleEventType {
case shutdownComputer
case restartComputer
case putComputerToSleep
case logoutUser
}
}
So far so good! If you are wondering why we define AppleEventType
inside the EventService
type, that’s to avoid cluttering the namespace and because the second enum is closely related to the first one.
Logic
The only function we want to write takes an AppleEventType
as parameter. We add the logic where the comment "logic will come here" lies.
extension EventService {
static func send(event eventType: AppleEventType) throws {
}
}
The function is throwing because we can get an error in the steps below, and we’ll have to forward it.
The skeleton is ready for implementation. To send an Apple event to the loginwindow
process, here are the three steps from the doc:
- Create an address targeting the
loginwindow
process.
- Create an Apple event with the provided event type using the created address in step 1.
- Send the Apple event created in step 2.
Doesn’t seem too hard! Of course we’ll have some pointers dance to do for each step.
Step 1: Create the address
To create an address, we have to call the function AECreateDesc
. It takes four parameters:
- The way to identify the process. We’ll give it a serial number here so we'll use the key keyProcessSerialNumber
.
- The serial number of the process we want to target. Held in a ProcessSerialNumber
struct (couldn’t find anything on this struct but that’s what the doc uses)
- The size (in bytes) of the serial number type. Given in Swift by MemoryLayout.size
- A pointet to an AEAddressDesc
value where the result will be copied.
Very often, C functions will return a value that indicates whether an error occurred. We’ll have to check that the returned value is noErr
else we’ll throw an error with a relevant message.
Before we call the function, we actually have to get the process serial number of loginwindow
. The doc states that we can obtain it by instantiating a ProcessSerialNumber
with the parameters (0, kSystemProcess
):
var loginWindowSerialNumber = ProcessSerialNumber(
highLongOfPSN: 0,
lowLongOfPSN: UInt32(kSystemProcess)
)
Prepare yourself for Int/Int15/Int32/UInt conversions by the way, all constants are Int
that have to be converted!
Here is the first step.
// 1
var loginWindowSerialNumber = ProcessSerialNumber(
highLongOfPSN: 0,
lowLongOfPSN: UInt32(kSystemProcess)
)
var targetDesc = AEAddressDesc()
var error = OSErr()
// 2
error = AECreateDesc(
keyProcessSerialNumber,
&loginWindowSerialNumber,
MemoryLayout<ProcessSerialNumber>.size,
&targetDesc
)
// 3
if error != noErr {
throw EventError(
errorDescription: "Unable to create the description of the app. Status: \(error)"
)
}
And some remarks:
1. We get the loginwindow
serial number, and instantiate an empty address and OSErr
for the function to fill them.
2. We create the address
3. If the function AECreateDesc
returns something else than a status not indicating an error, we throw an EventError
with an explanation.
We havent’t created the EventError
yet, so please add it below the AppleEventType
definition.
extension EventService {
struct EventError: LocalizedError {
var errorDescription: String?
}
}
To let you know, SplashBuddy uses a NSError
which is fine but when I am the one that will get an error, I prefer to have something more custom. So here the error clearly states that something happened when creating the address of loginwindow
.
Step 2: Create the Apple Event
To make an Apple event, we will have to use the function AECreateAppleEvent
. It takes 6 parameters:
- An event class which is required to identify the event. I could not find anything else than kCoreEventClass
and it’s the one we are going to use.
- The event ID. That’s where we will indicate a shutdown, restart, put to sleep or logout event
- The address of the process the event is destined to. So we’ll pass the AEAddressDesc
created in step 1 (as a pointer)
- To differentiate events, it’s possible to provide a custom ID or to let the system make one automatically. We’ll take the second option for this parameter
- To group events, it’s possible to provide a unique ID here, but we’ll ignore that and pass a kAnyTransactionID
.
- Finally, a pointer where the resulting event should be written at.
For the event id (parameter #2), we have to pass an OSType
value to identify it. So please add the following to the AppleEventType
enum:
var eventId: OSType {
switch self {
case .shutdownComputer: return kAEShutDown
case .restartComputer: return kAERestart
case .putComputerToSleep: return kAESleep
case .logoutUser: return kAEReallyLogOut
}
}
We are binding the Foundation constants to identify the event type to the enum’s cases.
Now we are ready for step 2 implementation.
// 1
var event = AppleEvent()
error = AECreateAppleEvent(
kCoreEventClass,
eventType.eventId,
&targetDesc,
AEReturnID(kAutoGenerateReturnID),
AETransactionID(kAnyTransactionID),
&event
)
// 2
AEDisposeDesc(&targetDesc)
// 3
if error != noErr {
throw EventError(
errorDescription: "Unable to create an Apple Event for the app description. Status: \(error)"
)
}
- We create the event to get the result and call the
AECreateAppleEvent
function. Don’t worry aboutAEReturnID
andAETransactionID
. They respectively are type aliases forInt16
andInt32
. I think they are here for retro-compatibility and I use them like SplashBuddy does for more clarity. - As mentioned in the doc, we release the created address of
loginwindow
when we don’t need it anymore. Frankly I am not sure it’s needed since it’s a structure so I look about it with a circumspect eye (like that 🧐). But I may not understand why so let’s do what the doc asks. It’s not a big deal to call one function anyway. - Again, if we get a value that is an error, we throw a relevant message.
Everything’s ready for step 3!
Step 3: Send the event
The function we need here is AESendMessage. It takes 4 parameters:
- The event to send
- A pointer to a reply to fill of type AppleEvent
- The mode to send the message. We could specify that we want to wait for a reply, or to queue it to an event queue. But here we don’t care about it so we’ll pass kAENoReply
. It seems that we can work on the bits to also specify other flags like for the interaction but it’s not required.
- The time our app is willing to wait to get a response. This is provided in ticks of the CPU. The doc advises to pass the default value (about one minute), but as SplashBuddy, we’ll pass an arbitrary value of 1000 ticks. I don’t think it’s relevant in our use case since we are sending specific events.
Here is the implementation.
// 1
var reply = AppleEvent()
let status = AESendMessage(
&event,
&reply,
AESendMode(kAENoReply),
1000
)
// 2
if status != noErr {
throw EventError(
errorDescription: "Error while sending the event \(eventType). Status: \(status)"
)
}
// 3
AEDisposeDesc(&event)
AEDisposeDesc(&reply)
- We send the event, getting the response in
reply
. - If we get an error, we throw it.
- We release the
event
andreply
variables as stated in the doc.
And that was the last step! Here is the final send(event:)
implementation.
extension EventService {
static func send(event eventType: AppleEventType) throws {
// target the login window process for the event
var loginWindowSerialNumber = ProcessSerialNumber(
highLongOfPSN: 0,
lowLongOfPSN: UInt32(kSystemProcess)
)
var targetDesc = AEAddressDesc()
var error = OSErr()
error = AECreateDesc(
keyProcessSerialNumber,
&loginWindowSerialNumber,
MemoryLayout<ProcessSerialNumber>.size,
&targetDesc
)
if error != noErr {
throw EventError(
errorDescription: "Unable to create the description of the app. Status: \(error)"
)
}
// create the Apple event
var event = AppleEvent()
error = AECreateAppleEvent(
kCoreEventClass,
eventType.eventId,
&targetDesc,
AEReturnID(kAutoGenerateReturnID),
AETransactionID(kAnyTransactionID),
&event
)
AEDisposeDesc(&targetDesc)
if error != noErr {
throw EventError(
errorDescription: "Unable to create an Apple Event for the app description. Status: \(error)"
)
}
// send the event
var reply = AppleEvent()
let status = AESendMessage(
&event,
&reply,
AESendMode(kAENoReply),
1000
)
if status != noErr {
throw EventError(
errorDescription: "Error while sending the event \(eventType). Status: \(status)"
)
}
AEDisposeDesc(&event)
AEDisposeDesc(&reply)
}
}
Don’t forget that the code can also be found on the gist.
Add the entitlements
Just before we can send events, we have to add the following key to the app entitlement:
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.loginwindow</string>
</array>
Try it
If you want to try it, here is a simple SwiftUI view with a picker and a button. You’ll first have to extend EventService.AppleEventType
to be CaseIterable
and Identifiable
:
extension EventService.AppleEventType: CaseIterable, Identifiable {
var id: String { rawValue }
}
Then use the view.
struct ContentView: View {
@State private var eventType: EventService.AppleEventType = .shutdownComputer
var body: some View {
VStack {
Picker("Action", selection: $eventType) {
ForEach(EventService.AppleEventType.allCases) { event in
Text(event.rawValue).tag(event)
}
}
HStack {
Spacer()
Button {
do {
try EventService.send(event: eventType)
} catch {
print(error, error.localizedDescription)
}
} label: {
Text("Send")
}
}
}
.padding()
.frame(width: 300, height: 100)
}
}
Thanks to the authors of SplashBuddy for having translated the code from Objc to Swift, I find it to be more pleasant to read!
📡
I hope you found this tip useful! If so, feel free to share it on Twitter. If you want to reach out to me or to know when new posts are available, you can find me on Twitter. Also you can send me an email.