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!

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)

Amazing.

I wanted to upgrade my privileged helper form SMJobBless to SMAppService.daemon.

Read the existing code from UpdatingYourAppPackageInstallerToUseTheNewServiceManagementAPI, ok it was nice and simple.

Well I wanted to do a bit more, after all apple tells me SMJobBless is deprecated and I should look into the SMAppService class

So I made this change

    class func register() {
        let service = SMAppService.daemon(plistName: "com.xpc.example.agent.plist")

        do {
            try service.register()
            print("Successfully registered \(service)")
        } catch {
            print("Unable to register \(error)")
            exit(1)
        }
    }

Made sure to have the plist under $APP.app/Contents/Library/LaunchDaemons/com.xpc.example.agent.plist

Boom, I immediately hit the wall. Apple sends me a not so useful error

Unable to register Error Domain=SMAppServiceErrorDomain Code=3 "No such process" UserInfo={NSLocalizedFailureReason=No such process}

Common guys, instead of riddles, give us better examples, more examples, that cover more than 1% of the functionality.

I'm struggling to understand expectations on happy path and posting here for others in case they wonder.

  1. I have no sand boxed apps
  2. I create valid/signed/notarized MyTest.pkg with a post install script
  3. Distribute the package to end users
  4. They install it sudo installer -verbose -pkg MyTest.pkg -target / will install my new app
  5. End user runs the new app, which calls /Applications/MyTest.app/Contents/MacOS/MyTest register
  6. We get Operation not permitted
  7. This will also add an entry on the System Settings -> General - > Open at Login or ask the user to enable this item on right corner user notifications.
  8. Since we failed on 5, we call /Applications/MyTest.app/Contents/MacOS/MyTest status
  9. We get SMAppServiceStatus.SMAppServiceStatusRequiresApproval
  10. Now our app asks the user to allow our item that was magically added on the step 7
  11. We are good to go and can now call our elevated privileged app or XPC.

After a lot of fiddling with this new API, this seems to be making sense. Apple does not want us to do all this and not give the end user a final say.

After all this work DO I still have to ask the end user to allow full disc access ?

What’s the intended use case your product? If this something primarily installed at managed sites? Or something that normal users will install themselves?

Share and Enjoy

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

Migrating from pkg installer to Service Management
 
 
Q