Reputation: 189
Working on my first macOS Launch Agent using XPC
.
I need the process that is delivering the service to be started by launchd
and to then stay running until the client process that made the initial call is closed.
I have set KeepAlive
to true
in the Launch Agent's plist
but obviously this means the process is still alive even after the client process that made the initial call has ended.
In the documentation in launchd.plist(5)
it states that "a dictionary of conditions may be specified to selectively control whether launchd
keeps a job alive or not"... does someone know what dictionary is being referred to here and how to implement the conditions?
Edit
Adding code for context although I would stress this all works and behaves as I expect it to (setup a connection to start the service).
Thanks to rderik for providing example code (https://github.com/rderik/rdConsoleSequencer).
// Connecting to the service from the client...
let connection = NSXPCConnection(machServiceName: "com.name.servicename")
connection.remoteObjectInterface = NSXPCInterface(with: MyXPCProtocol.self)
connection.resume()
let service = connection.remoteObjectProxyWithErrorHandler { error in
print("Received error:", error)
} as? MyXPCProtocol
// Service main.swift ...
let listener = NSXPCListener(machServiceName:
"com.name.servicename")
let delegate = ServiceDelegate()
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()
// Service class...
@objc class MyXPC: NSObject, MyXPCProtocol {
// My service functions...
}
// Service delegate...
class ServiceDelegate: NSObject, NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let exportedObject = MyXPC()
newConnection.exportedInterface = NSXPCInterface(with: MyXPCProtocol.self)
newConnection.exportedObject = exportedObject
newConnection.resume()
return true
}
}
// Service protocol
@objc(MyXPCProtocol) protocol MyXPCProtocol {
// My protocol functions...
}
// User LaunchAgents 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>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.name.MyXPC</string>
<key>Program</key>
<string>/mypath.../</string>
<key>MachServices</key>
<dict>
<key>com.name.myservice</key>
<true/>
</dict>
</dict>
</plist>
Upvotes: 0
Views: 413
Reputation: 189
Thanks to rderik for help with this.
An approach here is to use PathState to evaluate whether the process should be kept running. You can create a file when the app launches that has a path that corresponds with a key in the Launch Agent, where the key is associated with PathState. If PathState is set to true and the Launch Agent process is started, it will stay running.
Then when the client app process is ending, remove the file and the Launch Agent process now has a way to end as well.
There is still something missing from my implementation because the Launch Agent process does not seem to end automatically when the client app is closed - please add a comment if you know how to fix this.
Example Launch Agent plist including PathState key:
<?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>KeepAlive</key>
<dict>
<key>PathState</key>
<dict>
<key>/path_to_temp_file</key>
<true/>
</dict>
</dict>
<key>Label</key>
<string>com.name.MyXPC</string>
<key>Program</key>
<string>/path_to_service/</string>
<key>MachServices</key>
<dict>
<key>com.name.servicename</key>
<true/>
</dict>
</dict>
</plist>
Create and remove file at start and end of app lifecycle:
@main
class AppDelegate: NSObject, NSApplicationDelegate {
let fileManager = FileManager.default
let path: String = "/path_to_temp_file"
let contents = Data()
func applicationDidFinishLaunching(_ aNotification: Notification) {
fileManager.createFile(atPath: path, contents: contents, attributes: [:])
}
func applicationWillTerminate(_ aNotification: Notification) {
let urlPath = URL(fileURLWithPath: path)
do {
try fileManager.removeItem(at: urlPath)
} catch {
print("Error removing serviceProcessTemp file: \(error)")
}
}
}
Note:
I would add that messing around with the plist structure can lead to errors and I found it useful to use plutil to check revised plist files were correct...
$ plutil -lint /path/to/propertylist.plist
Upvotes: 1