Daemon in an app with a self-update feature

We've got a non-sandboxed app with a built-in daemon that does some root-privileged things for us on occasion. We're using the newest SMAppService APIs, using NSXPCConnections for communications, and generally things work as expected. The daemon is set up to terminate when the parent app terminates.

Our app also has (and uses the daemon for) a self-update feature. Once the new app is downloaded, the daemon takes over, replaces the app in-place, terminates the old app and launches the new one.

However, after this update, the daemon no longer works.

Any other build & launch of the app will silently fail when trying to talk to the daemon. The XPC connection can be constructed as usual, no errors, the process goes along like it should app-side, but the daemon never actually launches and never responds.

I can imagine there could be a few rules being broken here with the self-update and the built-in daemon, but what would they be and how can we work within the rules?

Answered by DTS Engineer in 794920022
Yea, seems to work from the daemon:

“Seems” being the operative term here.

AppKit is the canonical daemon unsafe framework, as defined in TN2083 [1]. If you use it from a daemon the results you get are unspecified. If you depend implementation details like this, you undermine the long-term stability of your product.

In this specific case, for example, it’s not clear which GUI login session the app will launch in, or what happens if there are no GUI login sessions.

The best way to relaunch your app after the update is to have the app relaunch itself. I’d do something like this:

  1. Have the app download the update.

  2. Have it message the daemon to prepare the update.

  3. At this point the daemon gets everything ready. It’s also a good point to check the update’s provenance.

  4. Throughout this process the app is running normally. Once the daemon is done with the previous step, have the app spawn a small executable and then terminate. The key thing about this small executable is that it has no dependency on the app’s bundle, so the next step can’t cause it grief.

  5. That executable then tells the daemon to commit the update. This replace’s the app’s bundle on disk.

  6. Once that’s done, the executable uses NSWorkspace to relaunch the app.

  7. And terminates.

Share and Enjoy

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

[1] TN2083 is old, and some of the details have changed over the years, but the core message is still valid.

Is your daemon embedded within your app, using the BundleProgram feature?

Share and Enjoy

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

Indeed it is, with the value being Contents/Resources/<daemon name>.

Would I be on the right path in suspecting that because the daemon's binary or wrapper or something else changed that the authorization for it becomes invalid?

This is a tricky issue because the system has to balance convenience (running your daemon from your bundle, rather than forcing to to install it somewhere) and security (making sure it can’t be tricked into running the wrong daemon).

During the update process, after the daemon has moved the app into place, does it then terminate itself?

Also, are you testing this on your development machine? Or in a ‘clean’ environment?

I recommend that you run tests like this as a user would, that is:

  1. Set up a machine that’s never installed your app before.

  2. Install version N.

  3. Upgrade to version N+1.

  4. See it it works.

I generally use a VM for this, so I can restore to a clean state between each test.

Testing this stuff on development machines, or even machines that have seen lots of development builds of your app, is less than ideal due to the persistent nature of the various databases that track app state (Launch Services, Gatekeeper, and background tasks).

Share and Enjoy

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

The daemon doesn't terminate itself explicity. Due to the way it's set up, we assumed when the parent app terminates, the daemon will terminate as well. However, this isn't exactly clear, as the daemon also launches the new app after it's confirmed that the old app has terminated... a Catch-22 of sorts?

I've been building this on my development machine, but have discovered VirtualBuddy, so I will try it.

the daemon also launches the new app after it's confirmed that the old app has terminated

Say what!?! How is your daemon launching your app? Doing that while following macOS’s execution context rules [1] is… well… tricky.

Share and Enjoy

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

[1] Something I talk about in depth in the old, but still surprisingly relevant, Technote 2083 Daemons and Agents.

I know, right? I believe we may be taking advantage of a RunLoop queuing?

After we send the message to terminate the old app, we use an AsyncSequence to KVO a change to isTerminated on the NSRunningApplication. Once that happens, we call NSWorkspace.shared.openApplication on the new app. It works.

My guess is that the termination message doesn't get to the daemon right away, it's queued up in its RunLoop until the terminate/launch procedure finishes?

we call NSWorkspace.shared.openApplication on the new app.

You’re doing this from within the daemon itself?

Share and Enjoy

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

Yea, seems to work from the daemon:

// launch new app
if relaunch {
   let configuration = NSWorkspace.OpenConfiguration()
   configuration.allowsRunningApplicationSubstitution = false
   configuration.createsNewApplicationInstance = true
   NSWorkspace.shared.openApplication(at: appURL, configuration: configuration) { app, error in
...
Yea, seems to work from the daemon:

“Seems” being the operative term here.

AppKit is the canonical daemon unsafe framework, as defined in TN2083 [1]. If you use it from a daemon the results you get are unspecified. If you depend implementation details like this, you undermine the long-term stability of your product.

In this specific case, for example, it’s not clear which GUI login session the app will launch in, or what happens if there are no GUI login sessions.

The best way to relaunch your app after the update is to have the app relaunch itself. I’d do something like this:

  1. Have the app download the update.

  2. Have it message the daemon to prepare the update.

  3. At this point the daemon gets everything ready. It’s also a good point to check the update’s provenance.

  4. Throughout this process the app is running normally. Once the daemon is done with the previous step, have the app spawn a small executable and then terminate. The key thing about this small executable is that it has no dependency on the app’s bundle, so the next step can’t cause it grief.

  5. That executable then tells the daemon to commit the update. This replace’s the app’s bundle on disk.

  6. Once that’s done, the executable uses NSWorkspace to relaunch the app.

  7. And terminates.

Share and Enjoy

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

[1] TN2083 is old, and some of the details have changed over the years, but the core message is still valid.

OMG you're totally right.

Updating an app doesn't need root privileges at all, it just needs something external.

I moved all of the "terminate + replace + relaunch" code in a separate SwiftUI app that I added as an "auxiliary executable" into the bundle, then just referenced that via Bundle.main, sent along the paths as process arguments, and it does its thing very quickly!

I still need to check to see if the daemon is intact after a replacement, I'll post what I find.

Updating an app doesn't need root privileges at al

Well, it might.

By default applications are in the Applications folder and that’s only writeable by admin users:

% ls -ld "/Applications"
drwxrwxr-x  78 root  admin  2496 11 Jul 18:01 /Applications

So, if a non-admin user tries to update then you’ll need to escalate privileges.

The Mac also lets users put apps wherever they want, and it’s possible that the directory might have tighter restrictions.

ps Have you looked at Sparkle? A significant fraction of non-App Store apps use it for updating. And even if you don’t use that code, you can at least use it as a guide to understanding the problem.

pps I wish that Apple had a better story for updating non-App store apps. If you agree, feel free to file an enhancement request for that.

Share and Enjoy

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

Yes, you are correct about the non-admin user. In our case, I think it's either rare or the non-admin user shouldn't be able to update the app in the first place, so I think that's a fair restriction.

We have looked into Sparkle, yes. It was one of the first things we suggested, but we had a few showstoppers: we have a cross-platform core in the app, and it takes care of the downloading, so we only need to apply the downloaded package and not manage the whole flow.

That said, I DID study the heck out of it when you mentioned the executable, and that's where I saw the separate Installer app and it all began to make sense.

I will indeed file an enhancement request - and thank you so much for the guidance.

Daemon in an app with a self-update feature
 
 
Q