Migrating from pkg installer to Service Management

Hello,

we are currently working on a plan to migrate our app suite from Developer ID binaries inside a simple pkg installer to macOS app store distribution.

The reason we are using an installer is that there are multiple binaries inside that communicate via XPC and we need to install the respective launchd plist in /Library/LaunchDaemons and /Library/LaunchAgents:

  • 1 root daemon
  • 1 agent that has minimal UI and lives in the system menu bar
  • 1 embedded command line utility in user agent
  • 1 embedded FileProvider extension in user agent
  • 1 embedded Action Extension in user agent
  • 1 agent that only does OAuth stuff

Looking through Updating helper executables from earlier versions of macOS I can install the root daemon with SMAppService.daemon(plistName:) and the OAuth helper with SMAppService.agent(plistName:). For the main application I only found SMAppService.mainApp which does not accept a property list configuration. Therefore, I have no place to put my MachServices array and so the File Provider extension, the Action Extension, and the embedded command line utility have no way to talk to the user agent.

Currently, XPC is used in between these processes:

  • user agent -> root daemon
  • command line utility -> user agent
  • action extension -> user agent
  • file provider extension -> user agent
  • user agent -> file provider extension: that already works through NSFileProviderServicing

I know app-to-app communication only works through launchd for security reasons, but these applications are all part of the same app group (except the root daemon obviously).

My question is what is the proper way of starting the user agent so XPC from other binaries just work ™️?

Any input is much appreciated!

Replies

we need to install the respective launchd plist in /Library/LaunchDaemons and /Library/LaunchAgents:

That last one is going to be a challenge. SMAppService supports agents (via the agent(plistName:) method) but that installs it for the current user, that is, in ~/Library/LaunchAgents. The API has no way to install an agent for all users )-:

While we have an ER on file for this already (r. 92457638), if you’d like to see this added a recommend that you file your own, making sure to describe your specific requirements.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the answer, Quinn.

Technically, it's not a hard requirement to install plists in /Library/Launch{Agent|Daemons} because

  1. company issued macs are rarely shared
  2. if they are, a second user can just install the app again
  3. larger customers use a MDM solution anyway

Since you mention only the last one is going to be a challenge, I assume the others are smooth sailing? :)

Specifically, how would I establish XPC connection from the Action Extension and command line tool to our agent? I found another posts of yours where you suggest some magic ™️ bundle identifiers for the apps and xpc mach endpoint names. So I tried to prefix all my com.example.myapp bundle identifiers and xpc mach endpoint names with $(TeamIdentifierPrefix).com.example.myapp but that also didn't do the trick. As you explained in almost every XPC post, without launchd you are in for a lot of pain.

I do understand the security considerations and why every XPC endpoint must go through launchd, but for apps in an app group you already have the team identifier and code signatures.

So after fiddling around with it for quite some time we managed to get XPC to work properly. The key is to put all binaries in an app group, let's say $(TeamIdentifierPrefix)com.example.myapp.xpc and use that as a prefix for the mach service names passed to NSXPCListener(machServiceName:). So service1 would use $(TeamIdentifierPrefix)com.example.myapp.xpc.service1.

We then added a new target that contained our root daemon and user agent binaries in its Resources folder along with proper plists in Library/Launch{Agents,Daemons}.

However, when we tried to use register them, all we got was Error Domain=SMAppServiceErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedFailureReason=Operation not permitted}. Turns out when the app calling register() on the service is sandbox, the service itself must also be sandboxed. But even after disabling sandboxing for all targets it still failed with the same error on the latest macOS 13 and 14.

So we decided to stick with building ordinary pkgs because a) it's already working and b) at least pkgbuild and productbuild only do what they have been told. As a benefit, we don't need to spend hours fighting with sfltool dumpbtm and sfltool resetbtm because some service picked up something in Xcode's DerivedData and immediately registers it. Or even better, Xcode calling RegisterWithLaunchServices by itself.

We also don't have to sift through pages of forum posts looking for how stuff works because a) documentation is either lacking, scattered over many different places with sometimes slightly different information, or outright wrong (e.g. the documentation for SMAppService.register and .unregister)