Beyond Scripting in Swift: Running background activities

Having glimpsed inside Grand Central Dispatch (GCD) when I was trying to work out why my Time Machine backups had been becoming irregular, I was impressed with what little I saw. Apple has clearly put a lot of thought and effort into engineering a sophisticated system for scheduling and running background activities. As usual it is only minimally documented – the long-promised and often-cited GCD reference guide is still nowhere to be found eight years after GCD’s release – and the only clues as to how to use it are given in documents which don’t at first appear to be particularly relevant.

Messing with GCD is also not an easy task. I haven’t even bothered trying to access it from a Swift Playground, which would appear to be futile, and the only real way of knowing what is going on is through log entries, where you can see GCD’s component parts at work. So getting anything to work looked as if it was going to be daunting.

Apple and others do provide quite a lot of information about apps putting their own code blocks into dispatch queues. This is fine when managing your own tasks such as background rendering, but not particularly relevant to setting up independent user tasks to be run in the background. For these, the most detailed information is provided in the Energy Efficiency Guide for Mac Apps, rather than the out-of-date Concurrency Programming Guide (2009-12), which is more about using concurrency within your own app. The key class is NSBackgroundActivityScheduler, which is well documented but lacks any example project.

What I want seems simple: to be able to schedule repeated tasks, like backups, which can be run from the command line, using GCD’s ‘smart’ dispatcher rather than the old school of launchd. When it is more complete, I’d like to provide a system of installing tasks using Property List files, much as with LaunchAgents and LaunchDaemons, and strip away the human interface from my app. But for the moment, DispatchRider’s simple windowed interface will make life much easier during development.

dispatchrider1

Each window’s ViewController then has a top-level NSBackgroundActivityScheduler with the identifier co.eclecticlight.DispatchRider.tasks, declared thus:
let activity = NSBackgroundActivityScheduler(identifier: "co.eclecticlight.DispatchRider.tasks")

Then the Start button calls a private function which configures the activity to be scheduled, and passes it to GCD to be dispatched. The Stop button merely stops the scheduled activity with
activity.invalidate()

The function to set up and pass the activity is the nub of the whole app, so I’ll step through that in detail. It consists of two parts: the setup, which not only configures the optional settings but also tried to bring the data required for the activity into local variables; and the activity itself, which is delivered as a completion.

The setup

private func startTask() {

First, set the activity to repeat according to the checkbox:
if (repeatCheck.state == NSOnState) {
activity.repeats = true
} else {
activity.repeats = false
}

Then set the time interval. This is set in seconds, but as background activities slop around in minutes, I only give the user the benefit of using minutes:
activity.interval = TimeInterval(self.theRepeat * 60)

Quality of Service comes in four levels and a default, although these are translated into a range of integers which suggests there is greater flexibility, perhaps. Here we just use that selected by the user:
activity.qualityOfService = QualityOfService(rawValue: QoSconsts[qosPopup.indexOfSelectedItem])!

The last of the settings is the tolerance:
if (theTolerance > 0) {
activity.tolerance = TimeInterval(theTolerance)
}

GCD plugs these, suitably massaged, into a dictionary, which it prints out to the log when the activity is set up, for example:
20:34:49.798941 (com.apple.cts) [com.apple.xpc.activity.All] co.eclecticlight.GCDlaunch.check: <dictionary: 0x7fc6e85777e0> { count = 7, transaction: 1, voucher = 0x7fc6e8573ed0, contents =
"Repeating" => <bool: 0x7fff9a188aa8>: true
"_SequenceNumber" => <uint64: 0x7fc6e85772b0>: 1
"GracePeriod" => <int64: 0x7fc6e8557990>: 60
"Interval" => <int64: 0x7fc6e8518a30>: 120
"name" => <string: 0x7fc6e8577b40> { length = 32, contents = "co.eclecticlight.GCDlaunch.check" }
"Delay" => <int64: 0x7fc6e85605e0>: 60
"Priority" => <string: 0x7fc6e8577b70> { length = 11, contents = "Maintenance" }
}

Each activity which we add is given its own _SequenceNumber, starting from 1, which is associated with our name, here an earlier test app co.eclecticlight.GCDlaunch.check. The GracePeriod and Delay are derived from the Tolerance, or here set to defaults determined by the Interval. The Priority is determined from the Quality of Service, which defaults to Maintenance. These are all fed into the heuristic which GCD then uses to determine when to run our activity.

The remaining items then bring in more distant values to local variables, ready to feed into the completion:
let theCmd = commandText.stringValue
let theParamStr = paramsText.stringValue
let theParamList = paramsText.stringValue.components(separatedBy: "&arg:")

The completion

This is the code which will actually be packaged up and delivered to GCD as the activity to be scheduled. It starts by writing out the count to the window and incrementing the counter. These are purely to make it easier to see what is going on without looking at the logs all the time:
activity.schedule() { (completion:
NSBackgroundActivityScheduler.CompletionHandler) in
self.countText.stringValue = "\(self.result)"
self.result += 1

The activity then sets up the command to be run, together with any parameters to be passed when it is called, launches that task, waits until it is complete, and writes its own message to the log:
let task = Process()
task.launchPath = theCmd
if (theParamStr == "") {
task.arguments = []
} else {
task.arguments = theParamList
}
let outPipe = Pipe()
task.standardOutput = outPipe
task.launch()
task.waitUntilExit()
os_log("DispatchRider task ran.")

We don’t here do anything with the standard output; in future this needs to report any errors back through the log.

Finally, the completion has to complete by reporting that the activity has finished:
completion(NSBackgroundActivityScheduler.Result.finished)
}
}

That’s it. There’s no need for code blocks or any other intricacies, as the packaging is all achieved in the completion. One point to note (which Xcode will conveniently prompt you to do anyway) is that references to other objects within the ViewController must be preceded by self, I presume because of the lexical scope within the completion.

The whole of that function is shown in better format below.

dispatchrider2

Implementing an NSBackgroundActivityScheduler in Swift 3 is thus amazingly clean and simple. It’s just not explicitly documented. Again.