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:
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:
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:
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:
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
keyLabel/key
stringcz.macadmin.xpcdaemon/string
keyProgram/key
string/path/to/xpcdaemon/string
keyRunAtLoad/key
true/
keyMachServices/key
dict
keycz.macadmin.xpcanon.xpc/key
true/
/dict
/dict
/plist
Log output:
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