Notification/callback mechanism

Hi, Is there anyway to notify or callback App from XPC service? XPC service is mentoring the state of an object and when its state is changed, i need tp notify the App. Is it possible? Thanks

Answered by maju in 396694022

Thanks eskimo! Let me try and get back to you on this solution.

There are various ways you can do this. Let me start by explaining the standard XPC way, and then we can talk about easier alternatives.

XPC implements a request/response protocol. When a client connects to your service, it issues a request and may optionally expect a response. This makes it hard for the service to unilaterally notify the client of events.

There are two ways to handle this within XPC:
  • Long polling

  • Reverse connection



Long polling is just like HTTP long polling: The client issues a request to the service and the service doesn’t respond until it has something meaningful for the client to hear about.

Like HTTP long polling, this is a bit of a hack and it’s not something I generally recommend.



In contrast, the reverse connection approach is more sophisticated. Here the client creates an anonymous listener (-[NSXPCListener anonymousListener] [1]), gets an endpoint from that (endpoint), and uses an XPC message to send that to the service. The service can then create a connection from that endpoint (-[NSXPCConnection initWithListenerEndpoint:]). For this connection the service is the ‘client’, and thus it can issue requests.

There is one major gotcha here, namely denial of service (possibly deliberate, but more likely accidental). If the client stops processing requests from this reverse connection and the service keeps sending them, the requests will stack up in memory on the service side. Eventually the service will run out of memory and crash. Not good.

For this reason it’s important that the service not send unbounded requests to the client. A good way to handle this is to use a request/response pair, and wait for the response before sending any more requests.



Finally, for simple tasks it might be easier to use a different notification mechanism. For example, the service could simply post a Darwin notification (see the <notify.h> man page) saying that something has changed. The client would then respond to that notification by using XPC to get the latest state from the service.



Oh, wait, one more thing. NSProgress in XPC-transportable, so if you just want to get progress info (including finished notifications) you can use that.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"

[1] I’m going to use NSXPCConnection constructs here, although everything I talk about can also be done with the XPC C API.
Accepted Answer

Thanks eskimo! Let me try and get back to you on this solution.

This post is wonderful, and I'm now trying to establish my "back connection" from daemon-XPC-service back to its user-bound agents. One question though. I AM SURE I read somewhere in The XPC documentation that once established - an XPC connection is bi-directional? doesn't that mean that on some lower-level both parties (the "Daemon-XPC-Service" publishing itself via machServiceName and its (multiple) clients) can initiate "requests" on the same connection? Or maybe I got this wrong and the "bi-directional" part is only the "response" passing back for a "request" ? I'd like a clarification.

In my case, NSProgress (oh, lovely NSProgress) isn't enough, and I must pass a dictionary describing some work Deamon-XPC-Service has done that is related to the user's agent (client). Because I may have more than one XPC-client (actually, one per logged-in user) The global notification isn't best choice (unless I could pack there all the data I need to pass, together with the user-ID, so that all agents would listen, and filter for their own user-id)

Last - both global-daemon and user-agent can of course be killed/restarted/fail-to-launch because they lack some TCC permissions and so on - how is best to manage such scenario of several clients, each with its own normal and "back" XPC connection to the Daemon?

Could you clarify how "Client/Agent", after creating an anonymous listener and getting its endpoint, "uses an XPC message to send that to the service." ???

To send something to the service/daemon, Client/Agent side needs to already have established a connection? and how do you send an endpoint in an XPC message? how you obtain it on the other (Service/Daemon) side? and is the new connection-from-endpoint created on the service side unique and maintained for that specific Client/Agent? Can I have more than one?

To send something to the service/daemon, Client/Agent side needs to
already have established a connection?

If you’re using NSXPCConnection you can send an NSXPCListenerEndpoint object over the connection by simply including it as a parameter in one of the calls defined in your protocol.

is the new connection-from-endpoint created on the service side unique

Yes.

Can I have more than one?

Yes.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
OK. I got it to basically work as described (reverse connection approach). Now for the "gotcha"'s.
  • Agent (maintains anonymous listener) connects to Daemon (mach-service-named), calls the Daemon and registers its anonymous-listener's endpoint.

  • Daemon in turn creates the reverse-connection, and later uses it to convey information to the Agent using specific protocol and methods.

If my Agent goes away (killed, quits etc.) Daemon's connection for some reason doesn't know about it (neither interruption handler nor invalidation handler on the reverse-connection are being called). Is this also the source of the issue of stacking-up of pending requests in memory?

Since Daemon has the original connection (Agent to Daemon) it knows when agent connection goes away - its invalidated.
Can I make a practical assumption that if my original connection was invalidated - than the reverse-connection should be treated as dead too, and needs to be re-established?

Also, when using the reverse-connection, could I make use of the:

Code Block
- (id)synchronousRemoteObjectProxyWithErrorHandler:(void (^)(NSError *error))handler API_AVAILABLE(macos(10.11), ios(9.0), watchos(2.0), tvos(9.0));

To be sure I don't stack up requests? and if so... how to use it? I do not understand what's "synchronous" here --- this is just an accessor providing me a proxy.

If my Agent goes away (killed, quits etc.) Daemon's connection for
some reason doesn't know about it (neither interruption handler nor
invalidation handler on the reverse-connection are being called).

That is surprising. I would have expected that to happen.

I do not understand what's "synchronous" here --- this is just an
accessor providing me a proxy.

The resulting proxy will run requests synchronously, that is, when you make a call on the proxy the calling thread will block until the remote process has responded to that request.

This is intended to be used for client-to-service requests, not the other way around. Doing that is likely to deadlock your XPC Service.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[I always forget about the following so I’m posting it here for the benefit of folks in general but most of all Future Quinn™.]

A while back I wrote:

For this reason it’s important that the service not send unbounded
requests to the client. A good way to handle this is to use a
request/response pair, and wait for the response before sending any
more requests.

An even better option is scheduleSendBarrierBlock(_:). There’s no documentation for this (r. 75253743) but <Foundation/NSXPCConnection.h> has a great comment explaining how it works.

Share and Enjoy

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

XPC is bidirectional. Just use setExportedInterface and setExportedObject in your client. Then in the server when you call a method on the remoteObjectProxy you will receive the method call in the exported object in the client.

FYI this is how apps that use CoreLocation receive location updates from the locationd daemon. Use Hopper on the CoreLocation framework and take a look at _CLLocationManagerRoutineProxy createConnection for more detail.

`/* @class _CLLocationManagerRoutineProxy */
-(void)createConnection {
    rbx = self;
    rdi = self->_connection;
    if (rdi != 0x0) {
            [rdi release];
            *(rbx + 0x18) = 0x0;
    }
    rax = [NSXPCConnection alloc];
    rax = [rax initWithMachServiceName:@"com.apple.locationd.routine" options:0x1000];
    *(rbx + 0x18) = rax;
    if (rax != 0x0) {
            [*(rbx + 0x18) setExportedInterface:[NSXPCInterface interfaceWithProtocol:@protocol(CLLocationManagerRoutineClientInterface)]];
            [*(rbx + 0x18) setExportedObject:rbx];
            r14 = [*(rbx + 0x18) exportedInterface];
            r13 = objc_opt_class(@class(NSArray));
            objc_opt_class(@class(CLLocation));
            [r14 setClasses:[NSSet setWithObjects:r13] forSelector:@selector(didUpdateLocations:) argumentIndex:0x0 ofReply:0x0];
            rdx = [NSXPCInterface interfaceWithProtocol:@protocol(CLLocationManagerRoutineServerInterface)];
            [*(rbx + 0x18) setRemoteObjectInterface:rdx];
            r14 = [*(rbx + 0x18) serviceName];
            rdi = *(rbx + 0x18);
            var_78 = *__NSConcreteStackBlock;
            *(&var_78 + 0x8) = 0xffffffffc2000000;
            *(&var_78 + 0x10) = sub_911b;
            *(&var_78 + 0x18) = 0x71d40;
            *(&var_78 + 0x20) = r14;
            [rdi setInterruptionHandler:rdx];
            rdi = *(rbx + 0x18);
            var_50 = *__NSConcreteStackBlock;
            *(&var_50 + 0x8) = 0xffffffffc2000000;
            *(&var_50 + 0x10) = sub_9133;
            *(&var_50 + 0x18) = 0x71d40;
            *(&var_50 + 0x20) = r14;
            [rdi setInvalidationHandler:&var_50];
            [*(rbx + 0x18) resume];
    }
    if ([rbx updating] != 0x0) {
            [[[rbx connection] remoteObjectProxy] startUpdatingLocation];
    }
    return;
}
`

There is info on how to do this in the docs here: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html#//apple_ref/doc/uid/10000172i-SW6-SW15

See the note that says:

Note: If you want to allow the helper process to call methods on an object in your application, you must set the exportedInterface and exportedObject properties before calling resume. These properties are described further in the next section.

Notification/callback mechanism
 
 
Q