Cross process URL bookmark

I am developing a background application that acts as a metadata server under MacOS written in Swift. Sandboxed clients prompt the user to select URLs which are passed to the server as security scoped bookmarks via an App Group and the metadata will be passed back. I don't want the I/O overhead of passing the complete image file data to the server. All the variations I have tried of creating security scoped bookmarks in the client and reading them from the server fail with error messages such as "The file couldn’t be opened because it isn’t in the correct format." Can anyone guide me in the right direction or is this just not possible?

Answered by DTS Engineer in 841318022

OK. A login item that you install this way runs for the duration of the user’s login session. That’s a key design point, because it opens up other options.

My advice here is that you:

  1. Use SMAppService to install a launchd agent.

  2. Have that agent publish a named XPC endpoint.

  3. Have client apps open the file they want to process.

  4. And then send the file descriptor to the agent using XPC.

This has a bunch of nice attributes:

  • In contrast to login items, launchd agents run on demand. That means that your background process will only run when there are active clients [1].

  • The fact that, in step 3, the client opens the file means that there’s no messing around with permissions. Clients can process any file that they can open.

This design assumes that you have multiple different client apps. If the client is always the app that contains the agent, you might want to look at an XPC service instead.

One thing to watch out for here is access control. For a client to connect to the agent’s named XPC endpoint:

  • It must share an app group with the agent, which in turn means it must be published by the same team.

  • Or it must not be sandboxed.

  • Or it must use a temporary exception entitlement to allow this access [2]. Such entitlements are fine in general but will cause complications if you try to publish the client on the App Store.

Share and Enjoy

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

[1] In reality, macOS only terminates idle agents when the system is under memory pressure, so you won’t see it immediately terminate as soon as the last client goes away.

[2] For more on that, see the links in App Sandbox Resources.

Sandboxed clients prompt the user to select URLs which are passed to the server as security scoped bookmarks via an App Group and the metadata will be passed back.

This won’t work. You can’t pass security-scoped bookmarks between processes.

However, it’s very likely that there is a way to achieve your overall goal. Before I get into details, I want to clarify one point: Does your background process need persistent access to these files? Or does it just read the file, do its job, and then no longer needs to access the file again?

Share and Enjoy

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

The background process only needs access to the files while the metadata are extracted - nothing needs to persist. If the client wanted more or different metadata - even from the same image file - it would be acceptable to run the procedure again.

OK. In that case my general inclination is to not pass a file reference but rather a file descriptor. That is, have the client app open the file read-only and pass that file descriptor to the server. The server can then happily read the file regardless of permissions, sandboxing, or whatever.

To make this work you see to set up a proper XPC channel between the server and its clients. The best way to do that depends on a bunch of factors. In your current design, how does you “background application” get launched?

Share and Enjoy

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

The current method of launching the server process: try SMAppService.loginItem(identifier: "abcd").register() I am open to any suggestions.

Accepted Answer

OK. A login item that you install this way runs for the duration of the user’s login session. That’s a key design point, because it opens up other options.

My advice here is that you:

  1. Use SMAppService to install a launchd agent.

  2. Have that agent publish a named XPC endpoint.

  3. Have client apps open the file they want to process.

  4. And then send the file descriptor to the agent using XPC.

This has a bunch of nice attributes:

  • In contrast to login items, launchd agents run on demand. That means that your background process will only run when there are active clients [1].

  • The fact that, in step 3, the client opens the file means that there’s no messing around with permissions. Clients can process any file that they can open.

This design assumes that you have multiple different client apps. If the client is always the app that contains the agent, you might want to look at an XPC service instead.

One thing to watch out for here is access control. For a client to connect to the agent’s named XPC endpoint:

  • It must share an app group with the agent, which in turn means it must be published by the same team.

  • Or it must not be sandboxed.

  • Or it must use a temporary exception entitlement to allow this access [2]. Such entitlements are fine in general but will cause complications if you try to publish the client on the App Store.

Share and Enjoy

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

[1] In reality, macOS only terminates idle agents when the system is under memory pressure, so you won’t see it immediately terminate as soon as the last client goes away.

[2] For more on that, see the links in App Sandbox Resources.

Thanks a lot - I will give this a try - it is not a problem for all the clients to share the same app group and be published by the same team.

I have done something similar using an XPC service with a single client but that seems much more straightforward to implement.

Generating the named XPC endpoint from a Swift app appears more complicated and tutorials/examples hard to find.

ChatGPT points me to using launchctl from the command line to register a MachServices name but this would make it complicated to distribute the application.

I have a bunch of links to XPC info in my XPC Resources post.

In general, I recommend that you split this problem in three:

  1. Implement XPC communication in general.

  2. Start XPC communication to a named XPC endpoint.

  3. Install the launchd job.


For the first part, use the loopback approach described in TN3113 Testing and Debugging XPC Code With an Anonymous Listener.

IMPORTANT While that technote assumes NSXPCConnection, this approach also works with low-level C API. It’s not yet supported by the low-level Swift API (r. 113356759).


For the second part, I generally create a launchd agent and install it using launchctl. I’m not actually sure that’s the best option, but it’s one I’m familiar with.

The agent property list would look something like this:

% plutil -p com.example.MyProduct.MyAgent.plist
{
  "BundleProgram" => "/path/to/my/agent"
  "Label" => "com.example.MyProduct.MyAgent"
  "MachServices" => {
    "com.example.MyProduct.MyEndpointName" => 1
  }
}

One potential source of confusion here is the difference between the label, which identifies the job, and the XPC endpoint name, which is what you connect to. These are commonly the same string, and hence the confusion. In this example I’ve gone out of my way to make them different. That’ll be the case in your final product because the XPC endpoint name needs to be ‘in’ your app group.

The launchd agent’s code would look something like this:

import Foundation
import os.log

func main() {
    let log = OSLog(subsystem: "com.example.MyProduct", category: "agent")
    os_log(.debug, log: log, "first light")
    let xpcListener = NSXPCListener(machServiceName: "com.example.MyProduct.MyEndpointName")
    … run the listener like you did with your anonymous listener …
    listener.activate()
    dispatchMain()
}

main()

Once you’ve got this working, you need to do the SMAppService packaging. There are two ways to set this up. The first looks like this:

% find MyContainerApp.app
MyContainerApp.app
MyContainerApp.app/Contents
MyContainerApp.app/Contents/MacOS
MyContainerApp.app/Contents/MacOS/MyContainerApp
MyContainerApp.app/Contents/MacOS/MyAgent
…
MyContainerApp.app/Contents/Library
MyContainerApp.app/Contents/Library/LaunchAgents
MyContainerApp.app/Contents/Library/LaunchAgents/com.example.MyProduct.MyAgent.plist

Here the agent is a standalone executable. The agent’s property list references it relative to the bundle:

% plutil -p SandboxedAgentApp.app/Contents/Library/LaunchAgents/com.example.MyProduct.MyAgent.plist 
{
  "BundleProgram" => "Contents/MacOS/MyAgent"
  "Label" => "com.example.MyProduct.MyAgent.plist"
  "MachServices" => {
    "SKMME9E2Y8.com.example.MyProduct.MyEndpointName" => 1
  }
}

Note how I’ve added my team ID to the front of XPC endpoint name. That’s so that I can put it in an app group:

% codesign -d --ent - MyContainerApp.app
…
[Dict]
    [Key] com.apple.security.app-sandbox
    [Value]
        [Bool] true
    [Key] com.apple.security.application-groups
    [Value]
        [Array]
            [String] SKMME9E2Y8.com.example.MyProduct
% codesign -d --ent - MyContainerApp.app/Contents/MacOS/MyAgent
…
[Dict]
    [Key] com.apple.security.app-sandbox
    [Value]
        [Bool] true
    [Key] com.apple.security.application-groups
    [Value]
        [Array]
            [String] SKMME9E2Y8.com.example.MyProduct

In this case I’m using a macOS-style app group ID. If you’re using an iOS-style app group ID — which is what we generally recommend these days — then MyAgent will need a provisioning profile, which means it must be packaged in an app-like wrapper. And that brings us to the second package layout, where you replace MyContainerApp.app/Contents/MacOS/MyAgent with MyContainerApp.app/Contents/MacOS/MyAgent.app. For advice on how to set up that packaging, Signing a daemon with a restricted entitlement (it talks about daemons but the same logic applies to agents).

For more background on app groups on the Mac, see App Groups: macOS vs iOS: Working Towards Harmony.

Once you have the packaging set up correctly, actually installing the agent is trivial:

let service = SMAppService.agent(plistName: "com.example.MyProduct.MyAgent.plist.plist")
try service.register()

But, yeah, this packaging step is quite convoluted, which is why I recommend that get the XPC side of this working first. That way you’ll only be debugging one problem at a time.

Share and Enjoy

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

Thanks again - plenty to think about here

Before I get too far into inter process communication I've been testing the concept of passing file descriptors using XPC by using an XPC Service (this should allow me to debug the XPC communication syntax).

Is it sufficient to pass a FileHandle?

Passing a FileHandle is straightforward and works well with the XPC Service but would it work across processes or do I need to find some way of passing a raw file descriptor?

Passing raw file descriptors seems much more complex - especially in a pure Swift application. I tried passing UnsafeMutablePointer<FILE> but that failed and crashed the application.

Is it sufficient to pass a FileHandle?

Yes. Not just sufficient, but required [1].

Other XPC APIs have similar facilities. For example, with the low-level C API you’d use xpc_fd_create.

You have to use these abstractions because file descriptors represent a capability [2], and these abstractions are the mechanism by which that capability moves between processes.

Share and Enjoy

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

[1] Historically this was the only option. These days you can probably find an alternative path that works by combining the low-level C API and NSXPCCoder, but that’s working way too hard.

[2] In the sense of capability-based security.

Cross process URL bookmark
 
 
Q