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
Accepted Reply
Thanks eskimo! Let me try and get back to you on this solution.
Replies
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.
Thanks eskimo! Let me try and get back to you on this solution.
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?
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?
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.To send something to the service/daemon, Client/Agent side needs to
already have established a connection?
Yes.is the new connection-from-endpoint created on the service side unique
Yes.Can I have more than one?
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
-
@eskimo You said "If you're using NSXPCConnection ....." but isn't that the only way to send an endpoint anywhere? You can't simply archive an endpoint up (with NSKeyedArchiver for instance) and send it anywhere; It'll return an error saying it can only be encoded with NSXPCCoder, which there's no API to use in that way.
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.
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.
That is surprising. I would have expected that to happen.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).
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.I do not understand what's "synchronous" here --- this is just an
accessor providing me a proxy.
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"
A while back I wrote:
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.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.
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.