More fun scripting with Swift and Xcode: cheating with AppleScript

One of the features that I wanted to build into LockRattler was to check whether software update was turned on. If someone notices that their silently updated security data files are out of date, one possible explanation is that software update has been turned off. If that’s the case, the user needs to be aware of the problem so that they can turn them back on.

The snag with this is that the command which reveals the status of software updating also requires root privileges. That is quite wrong, of course: no such informative command should ever require higher privileges, but I cannot fix that. So I needed to find a way of running the command
softwareupdate --schedule
with root privileges from within Swift.

You cannot use the command line trick of sudo when running a process in Swift. Neither is there any call which allows you to elevate privileges in the way that there used to be before OS X 10.6 or so. You have to go through the largely undocumented business of authorisation. I looked around for some sample code, and found a few fragments which allowed me to construct the sort of thing that I needed. I eventually arrived at a potential solution, which I tried out in a Swift playground.

scripting51

Given the dislike of playgrounds for additional windows, I thought that trying to run this was going to be doomed, but it all sort of worked. I didn’t manage to elevate my privileges and execute the command, but it produced an authentication dialog and everything ran without error.

So this was the code which I ended up pasting into the source of LockRattler:

var authRef: AuthorizationRef? = nil
let authFlags = AuthorizationFlags([])
let osStatus = AuthorizationCreate(nil, nil, authFlags, &authRef)
var myItems = [
AuthorizationItem(name: "co.eclecticlight.LockRattler.myRight1", valueLength: 0, value: nil, flags: 0),
AuthorizationItem(name: "co.eclecticlight.LockRattler.myRight2", valueLength: 0, value: nil, flags: 0)
]
var myRights = AuthorizationRights(count: UInt32(myItems.count), items: &myItems)
let myFlags : AuthorizationFlags = [.interactionAllowed, .extendRights]
var authRef2: AuthorizationRef?
let osStatus2 = AuthorizationCreate(&myRights, nil, myFlags, &authRef2)

By this stage, by my wild guessing, we should have privileges, so we then execute the command:

let task4 = Process()
task4.launchPath = theSWUpdCmd
task4.arguments = theSWUpdPar
let outPipe4 = Pipe()
task4.standardOutput = outPipe4
task4.launch()
let fileHandle4 = outPipe4.fileHandleForReading
let data4 = fileHandle4.readDataToEndOfFile()
task4.waitUntilExit()
let status4 = task4.terminationStatus

Finally, we free the authorization:

let osStatus3 = AuthorizationFree(authRef2!, authFlags)

This runs beautifully, without error, but fails to elevate privileges, so the command inevitably returns not an error, but the message that it requires root privileges.

Having fiddled around with it a bit, I realised that it was like trying the pin the tail on a donkey with both my eyes tied behind my back, and my hands blindfolded.

Sometimes quick and dirty solutions are the only way to get code to work. So I found some code fragments which, with a bit of gentle tweaking, would run the shell command from within an AppleScript do shell script … with administrator privileges, from within my Swift app. It’s tortuously indirect, but it only has to be run once, and its obscene inefficiency is here irrelevant.

The AppleScript solution runs thus:

var theASScript = "do shell script \"softwareupdate --schedule\" with administrator privileges"
sets up the command string dressed up in AppleScript.

@IBOutlet weak var theSWUpdOut: NSTextField!
is the text field that we’re going to put the response to that command into.

var appleScript = NSAppleScript(source: theASScript)
var eventResult = appleScript?.executeAndReturnError(nil)

submits the AppleScript for compilation and execution, and retrieves the result.

theSWUpdOut.stringValue = (eventResult?.stringValue)!
then inserts the result into the text box for display.

scripting52

scripting53

It worked first time, without so much as a glitch.

scripting54

There’s a profound irony in a workaround like this. Scripting in Swift may come to rely on its ability to execute shell commands via AppleScript. I think it’s time for Apple to come up with a better solution to do this in Swift throughout.