How macOS schedules and dispatches background tasks using CTS 3

Using Centralized Task Scheduling (CTS) from an app is fairly straightforward, once you’ve located its documentation. Don’t be mislead by Apple’s old Daemons and Services Programming Guide (last revised substantially in 2011-12), which describes XPC services being managed by launchd. Refer instead to the more recent Energy Efficiency Guide for Mac Apps, which is more up to date, although it too hasn’t been revised for several years. The most current account is given in the Foundation reference for NSBackgroundActivityScheduler.

In the past, you may have used the old XPC Activity interface to running a task in the background, using code such as
var criteria = xpc_dictionary_create(nil, nil, 0)
xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, true)
xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, XPC_ACTIVITY_INTERVAL_1_MIN)
xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_INTERVAL, XPC_ACTIVITY_INTERVAL_1_MIN)
xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY)
xpc_activity_register("co.eclecticlight.launch.check", criteria) { activity in
let task = Process()
task.launchPath = "/usr/local/bin/blowhole"
task.arguments = []
let outPipe = Pipe()
task.standardOutput = outPipe
task.launch()
task.waitUntilExit() }

That sets up an XPC dictionary with the controls for the new activity, then registers that activity, which simply runs the command blowhole at minute intervals. Using NSBackgroundActivityScheduler wraps this up in more understandable code.

First, create a new activity using an appropriate ID
let activity = NSBackgroundActivityScheduler(identifier: "co.eclecticlight.launch.check")
That ID remains constant between different launches of the app, and will be used by CTS in association with the user ID for the activity, for example as
501:co.eclecticlight.launch.check

For this example, we’ll set the interval to 2 minutes, with a tolerance of 0
let timeInterval = 2
let timeTolerance = 0

Now configure the new activity to repeat at that interval, using the default Quality of Service
activity.repeats = true
activity.interval = TimeInterval(timeInterval * 60)
activity.qualityOfService = QualityOfService.default
activity.tolerance = TimeInterval(timeTolerance * 60)

CTS uses activity.interval and activity.tolerance to determine the window within which the activity will be performed. If you set the interval to 60 minutes with a tolerance of 10 minutes, then the activity should be started between 50 and 70 minutes of the target time.

The current listed options for QualityOfService include:

  • NSQualityOfServiceUserInteractive (0x21), used for high-priority GUI-supporting activities
  • NSQualityOfServiceUserInitiated (0x19), used for other high-priority activies being run for the user
  • NSQualityOfServiceUtility (0x11), initiated for the user but of lower priority
  • NSQualityOfServiceBackground (0x09), for invisible background tasks
  • NSQualityOfServiceDefault (-1), no specific requirement, and typically set between NSQualityOfServiceUserInteractive and NSQualityOfServiceUtility.

Scheduling the activity with CTS is similar to using the old XPC Activity interface, but at the end there’s a call to the completion handler
activity.schedule() { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
let task = Process()
task.launchPath = "/usr/local/bin/blowhole"
task.arguments = []
let outPipe = Pipe()
task.standardOutput = outPipe
task.launch()
task.waitUntilExit()
completion(NSBackgroundActivityScheduler.Result.finished) }

When you want to stop that repeating task, all you do is call
activity.invalidate()

There is also a method of detecting whether an activity should be deferred if possible, by checking whether activity.shouldDefer is true. Your code can then call a halt and defer by calling its completion handler with
completion(NSBackgroundActivityScheduler.Result.deferred)
rather than .finished as above.

One common misconception is that, because this involves XPC Activity, there’s some requirement for activities to use XPC. As you can see from this example, and with DispatchRider, however you create them, these activities don’t have any such requirement.

CTS is something of a black box, though, which can puzzle the developer and sysadmin. As far as I know, there’s no tool which can ‘administer’ CTS and its lists of activities. There’s no way of seeing inside them, nor of listing activities which are currently being managed by DAS and CTS. Once you’ve added an activity which has activity.repeats set to true, your only control is to stop it using activity.invalidate(). You also only have indirect control over the priority afforded to an activity: once that has been set to one of the limited list offered by QualityOfService, there seems no scope to change that.

In practice, a lot of repeated tasks performed by apps could and should be run as activities by CTS. Similarly, many other background tasks should be reconfigured to do so, to ensure their efficient use of resources. As there are no other tools provided to support that, its lack of use shouldn’t be surprising.