NSXPCConnection from LaunchDaemon to LaunchAgent

Hi there!

Is it possible to use NSXPCConnection(machServiceName: x, options: y) from service running as LaunchDaemon (system context) to call methods on LaunchAgent running in Aqua Session (gui context)?

I have a running code where I connect to LaunchAgent from binary running in Xcode with elevated privileged. However when I launch my application as a LaunchDaemon I get:
Code Block
Agent could not connect: Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service on pid 0 named x was invalidated.

I suspect this could be happening by (XPC/launchd) design because each item is in different launchd domain. Could anyone shed more light on this?

If it is not possible to initiate XPC from LaunchDaemon to LaunchAgent, what kind of IPC alternative would you recommend to control the agent from the daemon?




Accepted Reply

I suspect this could be happening by (XPC/launchd) design because each
item is in different launchd domain.

That’s right. Moreover, initiating an XPC connection from a daemon to an agent makes no logical sense because there can be multiple instances of the agent running, so it’s not clear which agent you’d end up talking with.

The canonical way around this is to have each agent ‘check in’ the with the daemon as it comes up.

If you then need to ‘turn around’ the communication — so that the daemon makes the requests rather than the agent — you can do that using an anonymous XPC listener. Be aware, however, that there’s a risk involved in this. If the agent stops responding to messages it can gum up the daemon. If you do this sort of thing you must limit the number of outgoing requests that the daemon makes to any given agent (typically you set this limit to 1 by not issuing request N+1 until you’ve received the reply to request N).

This assumes that your agents are always running. If not, you can still use this check in model but you need a way for the daemon to trigger the agent to start and thus check in. Let me know if that’s the case and I can go into details.

Share and Enjoy

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

Replies

I suspect this could be happening by (XPC/launchd) design because each
item is in different launchd domain.

That’s right. Moreover, initiating an XPC connection from a daemon to an agent makes no logical sense because there can be multiple instances of the agent running, so it’s not clear which agent you’d end up talking with.

The canonical way around this is to have each agent ‘check in’ the with the daemon as it comes up.

If you then need to ‘turn around’ the communication — so that the daemon makes the requests rather than the agent — you can do that using an anonymous XPC listener. Be aware, however, that there’s a risk involved in this. If the agent stops responding to messages it can gum up the daemon. If you do this sort of thing you must limit the number of outgoing requests that the daemon makes to any given agent (typically you set this limit to 1 by not issuing request N+1 until you’ve received the reply to request N).

This assumes that your agents are always running. If not, you can still use this check in model but you need a way for the daemon to trigger the agent to start and thus check in. Let me know if that’s the case and I can go into details.

Share and Enjoy

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

I am quite new to this to this so pointing me into right direction helps immensely. :-)

My use case for XPC.

I am working on Swift reimplementation of https://github.com/macadmins/installapplications (Python). It is a bootstrap tool to setup other nonMDM management tools. It work like this:
  1. Mac enrolls into MDM (Usually during SetupAssistant via Automated Device Enrollment but enrollment can be also initiated by the user).

  2. MDM sends InstallEnterpriseApplicationCommand the device which installs the package containing the bootstrap tool.

  3. Bootstrap tool runs as a daemon. It downloads other items (scripts and packages) and executes them sequentially. Packages and most of the scripts needs to be installed/run by the root user so daemon is ideal for this. However we also need to run scrips for the user after the login (with user privileges and inside his gui session). This is where the LaunchAgent comes in.

  4. After the list of task is completed bootstrap tool uninstalls itself (removing the daemon and agent as well).

I need the daemon to be able to instruct the agent to run user scripts and usually wait until they finish (I use synchronousRemoteObjectProxyWithErrorHandler for this in my current code). Agent is launched either by launchd automatically (during the login) or by the package postinstall script if we install the package when there is already logged in user.

If I understand usage of anonymous XPC listener correctly:
  1. Daemon will listen for incoming XPC requests (Agent checkin-in).

  2. Agent is going to use NSXPCConnection.Options.privileged to be able to connect to the daemon.

  3. When Agent checks in it sends the anonymous XPC listener endpoint to the daemon.

  4. Daemon can now use the endpoint to issue XPC calls to the Agent.

Alternative to this would be Agent polling for work and reporting back the results.

Since I would like to keep most of the logic inside the daemon (make agent as dumb as possible) anonymous XPC listener looks like way to go.
Hi, have a similar scenario, and have been trying to do this for quite some time, to no avail (my "Agent" isn't a UI-session, but still runs as a daemon on the user's session).
  1. My global-daemon (runs in the system domain as root) receives connection from my "Agent",

  2. "Agent" can send messages to daemon successfully, and receive results.

Now if you could be so kind as to space a few lines of code explaining what it means to "send the anonymous XPC listener endpoint to the daemon" and how can Daemon use this endpoint. I tried several things and they didn't work for me so far.

In my case, it's not a "one time" job. Agent connects to Daemon and Daemon needs to regularly report its actions related to the Agent's user (I can have an agent for each user logged in).

My current state of code (not working) is:

Agent side creating connection:

Code Block ObjectiveC
// lazy property getter
- (NSXPCConnection *) myXPCConnection {
// Create the XPC Connection on demand
if (_myXPCConnection == nil) {
_myXPCConnection = [[NSXPCConnection alloc] initWithMachServiceName:preventionServiceLabel options:NSXPCConnectionPrivileged];
_myXPCConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(myXPCProtocol)];
// Attempt to create a bi-directional connection, so to receive reports from the XPC service.
_myXPCConnection.exportedInterface = NSXPCInterface interfaceWithProtocol:@protocol(OITPreventionXPCProtocol)];
_myXPCConnection.exportedObject = self;
_myXPCConnection.invalidationHandler = ^{ // on invalidation we simply nullify connection reference, so it will be re-created once app tries to use it again.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
self.myXPCConnection.invalidationHandler = nil;
self.myXPCConnection = nil;
os_log(preventionUILog, "connection has been invalidated");
#pragma clang diagnostic pop
}];
};
_myXPCConnection.interruptionHandler = ^{ // on interruption we simply nullify connection reference, so it will be re-created once app tries to use it again.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
self.myXPCConnection.interruptionHandler = nil;
self.myXPCConnection = nil;
os_log(preventionUILog, "connection interrupted");
#pragma clang diagnostic pop
}];
};
[_myXPCConnection resume]; // New connections always crated and starts in a suspended state
}
return _myXPCConnection;
}


Daemon side:

Code Block ObjectiveC
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
assert(listener == _listener);
assert(newConnection != nil);
BOOL accept = NO;
do {
// Verify client identity, only accept if client is code-signed by our teamID and if its signingID is of our Agent.
if (/* not allowed connection.... then */)
break;
accept = YES; // we accept connections from processes signed by our development team (proofpoint)
// now see who's calling.
if (/* it is Agent */)
[self.monitor agentConnected:newConnection];
// Configure incoming connection. First, set the interface that the exported object implements.
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(OITPreventionXPCProtocol)];
// Next set self to be the object the connection exports. All messages sent on the connection to this service will be sent to self to handle. The connection retains the exported object.
newConnection.exportedObject = self;
newConnection.invalidationHandler = ^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
[self.monitor agentDisconnectedForUser:newConnection.effectiveUserIdentifier];
#pragma clang diagnostic pop
}];
};
newConnection.interruptionHandler = ^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
os_log_info(myLog, "Agent Connection interrupted.");
#pragma clang diagnostic pop
}];
};
os_log_info(myLog, "XPCListener accepted XPC connection from pid: %d", newConnection.processIdentifier);
[newConnection resume]; // New connections always start in a suspended state, start it
} while(false);
return accept; // Returning YES from this method tells the system that you have accepted this connection.
}


and last - when Connection is established, and after Daemon receives a message call and replies to Daemon - I try to use the connection from the Daemon side like this:

Code Block ObjectiveC
-(void)reportPrevention:(MYEventESInfo * _Nonnull)eventInfo {
    uid_t user = eventInfo.processInfo.auid;
    NSXPCConnection *agentConnection = [self.connectedAgents objectForKey:@(user)];
    if (agentConnection != nil) {
        [agentConnection.remoteObjectProxy eventHandled:[eventInfo dictionary]]; // <<<<< HERE I CRASH WITH EXCEPTION
    }
    else {
        os_log(monitoringLog, "No Agent for Event: %{public}@", eventInfo );
    }
}


Objective-C exception : method 'eventHandled:' unknown to remoteObjectProxy.

Any idea what's wrong?

Now if you could be so kind as to space a few lines of code explaining
what it means to "send the anonymous XPC listener endpoint to the
daemon" and how can Daemon use this endpoint.

See this post.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
I put together proof of concept daemon and agent based on advice from @eskimo. Looks like it's working.
It's a bit naive implementation. However feel free to point out shortcomings.

Protocols:
Code Block swift
@objc(TestDaemonXPCProtocol) protocol TestDaemonXPCProtocol {
  func agentCheckIn(agentEndpoint: NSXPCListenerEndpoint, withReply reply: @escaping (Bool) -> Void)
}
@objc(TestAgentXPCProtocol) protocol TestAgentXPCProtocol {
  func doWork(task: String, withReply reply: @escaping (Bool) -> Void)
}

Daemon:
Code Block swift
@objc class AgentXPCConnector: NSObject, TestDaemonXPCProtocol{
    let connectionEstablished = DispatchSemaphore(value: 0)
    var connection: NSXPCConnection?
    func agentCheckIn(agentEndpoint: NSXPCListenerEndpoint, withReply reply: @escaping (Bool) -> Void) {
        if connection == nil {
            logger.log("Agent checking in")
            connection = NSXPCConnection(listenerEndpoint: agentEndpoint)
            connection!.remoteObjectInterface = NSXPCInterface(with: TestAgentXPCProtocol.self)
            connection!.resume()
            reply(true)
            connectionEstablished.signal()
        } else {
            logger.error("There is an agent alredy connected")
            reply(false)
        }
    }
}
class DaemonXPCServer : NSObject, NSXPCListenerDelegate {
    let agentResponder = AgentXPCConnector()
    func waitForConnection() {
        let timeOut = DispatchTime.now() + DispatchTimeInterval.seconds(86400)
        switch agentResponder.connectionEstablished.wait(timeout: timeOut) {
        case .success:
            logger.log("Connection established")
        case .timedOut:
            logger.error("Timed out while waiting for connection")
            exit(1)
        }
    }
    func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        newConnection.exportedInterface = NSXPCInterface(with: TestDaemonXPCProtocol.self)
        newConnection.exportedObject = agentResponder
        newConnection.resume()
        return true
    }
}
let logger = Logger(subsystem: "cz.macadmin.xpcanon", category: "daemon")
let server = DaemonXPCServer()
let listener = NSXPCListener(machServiceName: "cz.macadmin.xpcanon.xpc")
listener.delegate = server;
listener.resume()
logger.log("Daemon listening for agent connections")
server.waitForConnection()
logger.log("Creating agent remote object")
let agent = server.agentResponder.connection!.synchronousRemoteObjectProxyWithErrorHandler { error in
    logger.error("Problem with the connection to the agent \(String(describing: error))")
} as? TestAgentXPCProtocol
logger.log("Making the agent to do some work!")
agent!.doWork(task: "Work Work") { (reply) in
    if reply {
        logger.log("Work success!")
    } else {
        logger.log("Work fail!")
    }
}
logger.log("Daemon done")


Agent:
Code Block swift
@objc class AgentXPC: NSObject, TestAgentXPCProtocol{
    func doWork(task: String, withReply reply: @escaping (Bool) -> Void) {
        logger.log("Starting work")
        sleep(5)
        logger.log("Work DONE!")
        reply(true)
    }
}
class AgentAnonDelegate : NSObject, NSXPCListenerDelegate {
    func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        let exportedObject = AgentXPC()
        newConnection.exportedInterface = NSXPCInterface(with: TestAgentXPCProtocol.self)
        newConnection.exportedObject = exportedObject
        newConnection.resume()
        return true
    }
}
let logger = Logger(subsystem: "cz.macadmin.xpcanon", category: "agent")
/* Prepare Anonymous listenter endpoint */
let anonDelegate = AgentAnonDelegate()
let anonListener = NSXPCListener.anonymous()
anonListener.delegate = anonDelegate
anonListener.resume()
let anonEndpoint = anonListener.endpoint
/* Prepare connection to the daemon */
let daemonConnection = NSXPCConnection(machServiceName: "cz.macadmin.xpcanon.xpc", options: NSXPCConnection.Options.privileged)
daemonConnection.remoteObjectInterface = NSXPCInterface(with: TestDaemonXPCProtocol.self)
daemonConnection.resume()
let daemon = daemonConnection.synchronousRemoteObjectProxyWithErrorHandler { error in
    logger.log("Unable to connect to daemon")
} as? TestDaemonXPCProtocol
/* Try to checkin... forever! */
var connectedToDaemon = false
while !connectedToDaemon {
    daemon!.agentCheckIn(agentEndpoint: anonEndpoint) { (reply) in
        logger.log("Passed endpoint to the deamon")
        connectedToDaemon = true
    }
    sleep(1)
}
/* Nothing more to do here. Only doing work for the daemon */
logger.log("Agent is in the work loop")
RunLoop.main.run()


LaunchDaemon.plist:
Code Block plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>cz.macadmin.xpcdaemon</string>
        <key>Program</key>
        <string>/path/to/xpcdaemon</string>
        <key>RunAtLoad</key>
        <true/>
        <key>MachServices</key>
        <dict>
            <key>cz.macadmin.xpcanon.xpc</key>
            <true/>
        </dict>
</dict>
</plist>


Log output:
Code Block
daemon Daemon listening for agent connections
daemon Agent checking in
daemon Connection established
daemon Creating agent remote object
daemon Making the agent to do some work!
agent Passed endpoint to the deamon
agent Starting work
agent Agent is in the work loop
agent Work DONE!
daemon Work success!
daemon Daemon done