WOODY'S
FINDINGS

Programmatically logout a user in Swift

Tip

12 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:

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:

  1. Create an address targeting the loginwindow process.
  2. Create an Apple event with the provided event type using the created address in step 1.
  3. 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)"
    )
}
  1. We create the event to get the result and call the AECreateAppleEvent function. Don’t worry about AEReturnID and AETransactionID. They respectively are type aliases for Int16 and Int32. I think they are here for retro-compatibility and I use them like SplashBuddy does for more clarity.
  2. 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.
  3. 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)
  1. We send the event, getting the response in reply.
  2. If we get an error, we throw it.
  3. We release the event and reply 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.