Skip to content

Instantly share code, notes, and snippets.

@ABridoux
Last active March 30, 2023 13:40
Show Gist options
  • Save ABridoux/8efb76bdd4ab070d5ea4a18b33a34144 to your computer and use it in GitHub Desktop.
Save ABridoux/8efb76bdd4ab070d5ea4a18b33a34144 to your computer and use it in GitHub Desktop.
A service to send events to restart, shutdown, put to sleep or logout the computer.
// Free to use
// Written by Alexis Bridoux - https://github.com/ABridoux
import AppKit
import Foundation
/// Service to shut down, restart, or put the computer to sleep. Also log out the user.
///
/// ### Resources
/// - [Apple doc](https://developer.apple.com/library/archive/qa/qa1134/_index.html)
/// - Already in use in [SplashBuddy](https://github.com/macadmins/SplashBuddy/blob/main/SplashBuddy/Tools/LoginWindow.swift)
enum EventService {}
// MARK: - Logic
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)
}
}
// MARK: - Models
extension EventService {
enum AppleEventType: String {
case shutdownComputer = "Shut down the computer"
case restartComputer = "Restart the computer"
case asleepComputer = "Asleep the computer"
case logoutUser = "Logout the user"
var eventId: OSType {
switch self {
case .shutdownComputer: return kAEShutDown
case .restartComputer: return kAERestart
case .putComputerToSleep: return kAESleep
case .logoutUser: return kAEReallyLogOut
}
}
}
}
extension EventService.AppleEventType: CaseIterable, Identifiable {
var id: String { rawValue }
}
extension EventService {
struct EventError: LocalizedError {
var errorDescription: String?
}
}
@ABridoux
Copy link
Author

ABridoux commented Aug 11, 2021

Explanations

Read the article

How to use it

First it's required to add the following key to the app entitlements:

<key>com.apple.security.temporary-exception.apple-events</key>
<array>
    <string>com.apple.loginwindow</string>
</array>

Then

EventService.send(event: .restartComputer)

EventService.send(event: .logoutUser)

@fabienconus
Copy link

Is this still valid as of macOS 13 ? I tried using it but XCode complains that it can't find 'kSystemProcess' in scope (line 21).

@ABridoux
Copy link
Author

Is this still valid as of macOS 13 ? I tried using it but XCode complains that it can't find 'kSystemProcess' in scope (line 21).

It seems it's not anymore in Foundation (or I was mistaken). Works fine when you import AppKit. I am updating the gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment