How can my app use SMAppService to re-register a launch daemon when the app's location has changed?

I'm developing an app that uses SMAppService to register a launch daemon. I'm running into big troubles with having the daemon start up reliably because, over the course of testing, the location of my app bundle has changed. Various workflows for this app require testing it running from with /Applications, /Applications/Utilities, or in the project's build directories, which includes both debug and release builds. And I need to account for users moving the app bundle around.

The trouble is that the launch daemon seems to want to launch from the first path it was registered, not at the path it was last registered. For example, if my app originally registered it while located in /Applications/MyApp.app, then the launch daemon will only run from within an app bundle at that specific location, even if there is no app bundle at that path.

As per advice on these forums, I've tried accounting for this by unregistering the daemon, waiting two seconds, and then re-registering it. But in practice this isn't working. I can use sfltool dumbtm and see that the app is still registered in /Applications/MyApp.app and not its new location, and the launch daemon simply silently fails to launch -- SMAppService returns no error. sudo launchctl list displays the job as having status 78. If I set up a stdout/stderr redireciton in its job file, the files are empty.

(I'm aware that I can use sfltool resetbtm to completely reset all login items and background tasks and temporarily fix this issue at least on my own system, but I can hardly have my app use that as part of its standard installation procedure. I need some way of dealing with this that doesn't requiring reseting the state of every app that has a background item.)

I'm starting to pull my hair out dealing with this! (Figuratively, mind you. Not literally, at least not yet.) How can I get this to work? This API is starting to seem like a downgrade from placing a plist file in /Library/LaunchDaemons because at least that was able to work very reliably.

Answered by DTS Engineer in 913083022

So, yeah, SMAppService is gluing together two subsystems:

  • Launch Services, where keeps track of multiple instances of an app and has the concept of the default instance
  • launchd, which is all about fixed paths

This combo is fundamentally problematic, even if everything goes well.

In practice this isn’t usually as bad as it could be because most users maintain a single copy of your app and put it in the Applications folder. However, not all users do that.

And, oof course, your development machine will likely have multiple copies of your app, and thus you’re much more likely to see problems like this there.

Let’s start by talking about user machines. Many apps have a first launch check, one that checks whether they’re located in Applications and prompts the user to move the app there if they’re not. I’m not super enthusiastic about that concept in general, but if your app has an embedded launchd daemon I think it makes sense.

Also, it probably makes sense to have some sort of self-repair mechanism, so something that detects that things have gone wrong and either tells the user how to fix it or offers to fix it for them.

Second, there’s the question as to what to do about your development machine. Personally, I tackle that in one of two ways:

  • I do whatever it takes to speed up my build-run-debug cycle. Usually this is something hacky. You can find an example of that in Debugging a Network Extension Provider (different technology, similar issue).
  • When it comes to testing user scenarios, I do that in a VM. That way I can revert to a snapshot between tests. For example, I might have a snapshot for testing initial install and another snapshot for testing upgrade.

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

So, you’re using the BundleProgram property to tell SMAppService to locate your daemon relative to your app?

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

So, you’re using the BundleProgram property to tell SMAppService to locate your daemon relative to your app?

Yes, that's right. That launch daemon will start up, but only if my app bundle is at the file location associated with my app's bundle ID as printed by sfltool dumpbtm

So, yeah, SMAppService is gluing together two subsystems:

  • Launch Services, where keeps track of multiple instances of an app and has the concept of the default instance
  • launchd, which is all about fixed paths

This combo is fundamentally problematic, even if everything goes well.

In practice this isn’t usually as bad as it could be because most users maintain a single copy of your app and put it in the Applications folder. However, not all users do that.

And, oof course, your development machine will likely have multiple copies of your app, and thus you’re much more likely to see problems like this there.

Let’s start by talking about user machines. Many apps have a first launch check, one that checks whether they’re located in Applications and prompts the user to move the app there if they’re not. I’m not super enthusiastic about that concept in general, but if your app has an embedded launchd daemon I think it makes sense.

Also, it probably makes sense to have some sort of self-repair mechanism, so something that detects that things have gone wrong and either tells the user how to fix it or offers to fix it for them.

Second, there’s the question as to what to do about your development machine. Personally, I tackle that in one of two ways:

  • I do whatever it takes to speed up my build-run-debug cycle. Usually this is something hacky. You can find an example of that in Debugging a Network Extension Provider (different technology, similar issue).
  • When it comes to testing user scenarios, I do that in a VM. That way I can revert to a snapshot between tests. For example, I might have a snapshot for testing initial install and another snapshot for testing upgrade.

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

Let’s start by talking about user machines. Many apps have a first launch check, one that checks whether they’re located in Applications and prompts the user to move the app there if they’re not. I’m not super enthusiastic about that concept in general, but if your app has an embedded launchd daemon I think it makes sense.

Yes, I've already coded that behavior into my app. I agree it's not ideal, but my app may also include a system extension which would make being somewhere inside of /Applications a hard requirement. I was hoping though that the user might be able to choose where in Applications they put it, but it seems like it might just be a better idea to force it to go into the root of Applications, otherwise it will break if the user ever decides to move it later.
⠀

Also, it probably makes sense to have some sort of self-repair mechanism, so something that detects that things have gone wrong and either tells the user how to fix it or offers to fix it for them.

This is part here is really the crux of my question. Is there anything the app can do on its own to fix the situation? For example, is there a way to modify which copy of the app is registered as the default copy with Launch Services? SMAppService lacks the means to do so on its own.

Since you mentioned Launch Services, I dug a little deeper and found that if I dump the Launch Services database on my dev machine using:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump

...it doesn't have an entry for my app in the Applications folder. But that's where its launch daemon is trying to start from, and that's where the entry for it is listed if I run sfltool dumpbtm. This suggests to me that it's not actually the default copy from Launch Services that determines where it runs. Am I correct, and if so, any idea where it's actually getting the path of the app that's associated with the launch daemon?
⠀

Second, there’s the question as to what to do about your development machine...

I worked around this actually by making a symlink in /Applications that points to the copy of the app I'm currently working with. Not particularly elegant but it means I don't have to use sfltool resetbtm to fix it.

Written by guygizmo in 913357022

Is there anything the app can do on its own to fix the situation?

Hmmmm, I always assumed that using SMAppService to unregister and then re-register your daemon would fix this. But I suspect you’ve already tried that and it didn’t work. Is that right?

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

Hmmmm, I always assumed that using SMAppService to unregister and then re-register your daemon would fix this. But I suspect you’ve already tried that and it didn’t work. Is that right?

Yes, it doesn't update the path of the app bundle. I'll mention that I'm testing in macOS 13 since that's the minimum system my company still supports, so I wonder if this is an issue that's been fixed in later macOS releases.

This API is starting to seem like a downgrade from placing a plist file in /Library/LaunchDaemons because at least that was able to work very reliably.

I'm sure that if you move the executable to a new location, the old system would have also failed to find it.

it doesn't have an entry for my app in the Applications folder. But that's where its launch daemon is trying to start from, and that's where the entry for it is listed if I run sfltool dumpbtm. This suggests to me that it's not actually the default copy from Launch Services that determines where it runs. Am I correct, and if so, any idea where it's actually getting the path of the app that's associated with the launch daemon?

Does it matter? Trying to code for underlying implementation logic usually has a poor outcome. I've seen many funky little aspects to launchd. Any one of them could potentially be the cause. But because it has so many of these oddball behaviours, it's very unlikely to be changed. And if it does get changed, it'll be certain your break your hand-crafted solution.

For an app that has system-level dependencies like this, there's nothing wrong with restricting it to /Applications. Some people will complain, of course. They always do. You can simply blame Apple. People will generally accept that. That's why so many developers use that strategy.

In the past, a launch daemon would have probably required a package installer. You can still do that, and install into /Library/LaunchDaemons and some other stable, hidden path.

And you can also check for error conditions. Check the location you're running from and then check the path that launchd is trying to use. Tell the user about it and let them handle it.

Oh, wait, you'd have to use launchctl to get the path, wouldn't you? Sorry, that's not API. But if only there was an API for such things. There used to be. It's still there, but deprecated and never replaced. Maybe next year!