Creating a Launch Agent that provides an XPC service on macOS using Swift Oct 21 2019 Latest Update: Apr 11 2021
Last week we discussed how to build XPC Services(the .xpc
bundles) inside your macOS applications. This week we are going to explore how to provide XPC services that can be used from other applications or tools.
To make the XPC service available to other processes, we are going to create a launch agent. So let's start by understanding how Launch Agents work.
* You can find the code for the Launch Agent in this GitHub repository
* You can find the code for the tool to connect to the XPC service in this GitHub repository
Table of Contents
Understanding Launch Agents
At the centre of all service management and process loading is the Launch Daemon (launchd(8)
). The Launch Daemon first task is to boot the system, it's the process with PID(Process ID) 1, and all other processes spawn from it. Once loaded, it is in charge of managing and launching other daemons and agents.
In Apple's ecosystem, there is a difference between daemons and agents. A daemon is usually a process that runs in the background performing a specific task without user interaction. It is typically run as root when the system has boot up. An agent is a daemon that runs for the logged-in user, that means, it only runs when the user is logged in and with the user permissions. We are going to focus on agents.
launchd
uses multiple directories to hold daemons and agents configurations. For agents:
/System/Library/LaunchAgents/
for system agents./Library/LaunchAgents/
for third party agents.~/Library/LaunchAgents/
for user defined agents.
The configuration files are written in PList
files, the plist definition can be found in launchd.plist(5)
man page.
Let's start by creating a simple Launch Agent to understand how agents work.
A basic Launch Agent to test the waters
We are going to create an Agent that checks and logs how much disk space is being used in a specific directory. Our agent will run every 10 seconds.
We are going to use a simple bash script to accomplish this. Create your script wherever is more convenient for you. I'll create it on my Desktop under the name duer.sh
(because we'll use the du(1)
command), add the following content:
1
2
3
#!/bin/bash
WATCH_DIR=~/Desktop/testDir
{ date | tr -d '\n' && printf " --" && du -sh ${WATCH_DIR} ;} >> ~/Desktop/duer.log
Make sure your script is executable and that your WACH_DIR
exists:
1
2
$ chmod u+x ~/Desktop/duer.sh
$ mkdir -p ~/Desktop/testDir
* Note: If you are using Catalina (macOS 10.15), you'll probably need to add bash
to the authorised apps for "Full Disk Access" in "Security and Privacy" inside System Preferences. To add it:
- Open System Preferences
- Go to Security and Privacy
- Scroll down to "Full Disk Access"
Open the bin directory on Finder. From your terminal, you can use the
open(1)
command.1
$ open /bin
Drag the bash executable to the System Preferences window.
If you had some problem with your daemons or agents after installing macOS 10.15, check if they are allowed to access your file system.
Ok, back to our Launch Agent.
Create the file com.rderik.duer.plist
(You can name it whatever you want, but reverse DNS naming is the convention) inside ~/Library/LaunchAgents/
directory with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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>com.rderik.duer</string>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>10</integer>
<key>Program</key>
<string>/Users/derik/Desktop/duer.sh</string>
</dict>
</plist>
The required Label
should be a unique identifier for our agent. Our script will run when loaded and also run every 10 seconds. Let's load it using launchctl(1)
command:
1
$ launchctl load ~/Library/LaunchAgents/com.rderik.duer.plist
You can verify it is running with the following command:
1
2
$ launchctl list | grep rderik
- 0 com.rderik.duer
And we can check the file duer.log
in our Desktop.
1
$ tail -f ~/Desktop/duer.log
Running that command will show us the output of our agent and display the log as it changes (you can stop it by pressing Control+C
, but leave it for now). Let's test that the script works when we add more files to the testDir
directory. You can create a new file in testDir
using the following command in a different shell :
1
$ dd if=/dev/zero of=~/Desktop/testDir/temp.txt bs=10m count=1
The dd
command will create a 10 megabytes file inside our directory, and the log will show this difference.
Everything should work correctly, you should see the log showing the new size of the directory.
Let's stop the tail
command and let's remove the agent from the Launch Daemon. You can "deregister" an agent using the following command:
1
$ launchctl unload ~/Library/LaunchAgents/com.rderik.duer.plist
That will unload it, and you can verify that it is no longer running with the following command:
1
$ launchctl list | grep rderik
If it is still running, you'll see it there. Else, you'll get the prompt again.
You can remove the file com.rderik.duer.plist
from your ~/Library/LaunchAgents/
directory, so it doesn't get loaded when you boot your computer later. And also you can remove the files from your Desktop, we won't use them again.
Daemons and agents run on-demand. This means that we load the configuration to launchd
then the Launch Daemon will be listening for any request to the registered daemons and agents, and spawn them when needed. It will also shut down daemons and agents if they are no longer required.
Ok, I hope that gives you a clear idea of how to add Agents to the Launch Daemon. Let's now create a Swift command-line application that provides an XPC service that other applications can connect to.
Creating an XPC service using Swift
I'll try not to repeat all the concepts behind XPC that we covered in our previous post XPC Services on macOS apps using Swift. But I encourage you to check the previous post if you need a refresher on XPC concepts.
I'll give a quick refresher, so we start with the same base.
XPC concepts summary
An XPC service is an Inter-Process-Communication(IPC) mechanism. XPC allows us to easily communicate processes by relying on launchd
to manage all the services and communication. Applications can have many XPC Services bundles inside the Contents/XPCServices/
directory, inside the Application bundle directory structure.
Each XPC Service bundle can provide services that are spawned on-demand by launchd
. This allows us to modularise our application, reducing the memory footprint, by having functionality split into the XPC Services. It also makes our main application more robust. If the process of the XPC Service crashes or is killed, it won't affect our main application. The XPC Service can be re-spawned by launchd
when needed. There are other benefits. You should read the previous post if you want to know more.
When we create the XPC Service bundle, we define the name of the service inside the Info.plist
so it can be located by our main application.
But the XPC service provided by our XPC Service bundle can only be accessed by our own application. What if we want to create a service that can be accessed by other processes? Well, that is what we are going to do just now, create an agent that provides an XPC service.
Creating a Launch Agent that provides an XPC service
Our Launch Agent will provide an XPC service that will receive a string and return a coloured string representation using Control Escape Sequences. The string will be displayed coloured if we are using a Terminal that supports ANSI/VT100 Escape Sequences.
To test the XPC service, we are going to create a command-line tool that displays the text entered by the user in red and green. We want to be able to use this service without having to know anything about Control Escape Sequences. Check this link If you want to know more about Control Escape Sequences.
We are going to use the Swift Package Manager, so let's get started. Create a directory called rdConsoleSequencer
and initialise it with SPM:
1
2
3
$ mkdir rdConsoleSequencer
$ cd rdConsoleSequencer
$ swift package init --type executable
The general workflow for creating the XPC service is the same as with the XPC Service bundle.
(1) We create a listener and (2) set its delegate object. The delegate is in charge of accepting and setting up new incoming connections. Once our listener has a delegate, we call resume that will indicate to our listener to (3) start “listening” for connections.
The difference is that we are going to expose a Mach Service that can be accessed from other applications. Our main for the XPC Service bundle looked like this:
1
2
3
4
5
6
7
import Foundation
let listener = NSXPCListener.service()
let delegate = ServiceDelegate()
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()
We create the listener by calling NSXPCListener.service()
this returns the singleton listener for the XPC Service bundle. But we can also create the listener using the initialiser init(machServiceName:)
this initialises a listener for a Launch Agent or Launch Daemon. So our main.swift
will look like this:
1
2
3
4
5
6
7
import Foundation
let delegate = ServiceDelegate()
let listener = NSXPCListener(machServiceName: "com.rderik.ConsoleSequencerXPC" )
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()
The rest stays the same. Let's create our service delegate class. Create a file with the name ServiceDelegate.swift
inside the Sources directory, and add the following content:
1
2
3
4
5
6
7
8
9
10
11
import Foundation
class ServiceDelegate : NSObject, NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let exportedObject = ConsoleSequencerXPC()
newConnection.exportedInterface = NSXPCInterface(with: ConsoleSequencerXPCProtocol.self)
newConnection.exportedObject = exportedObject
newConnection.resume()
return true
}
}
The interface of our service will be defined by our protocol ConsoleSequencerXPCProtocol
. As before the exported object is an instance of an object that implements the ConsoleSequencerXPCProtocol
. Let's define our protocol first. Create a file ConsoleSequencerXPCProtocol.swift
inside our Sources directory, and add the following content:
1
2
3
4
5
6
import Foundation
@objc(ConsoleSequencerXPCProtocol) protocol ConsoleSequencerXPCProtocol {
func toRedString(_ text: String, withReply reply: @escaping (String) -> Void)
func toGreenString(_ text: String, withReply reply: @escaping (String) -> Void)
}
Here we define the two functions that our XPC service provides. Let's now create a class that implements the protocol. Create a file with the name ConsoleSequencerXPC.swift
inside the Sources directory, and add the following content:
1
2
3
4
5
6
7
8
9
10
11
import Foundation
@objc class ConsoleSequencerXPC: NSObject, ConsoleSequencerXPCProtocol{
func toRedString(_ text: String, withReply reply: @escaping (String) -> Void) {
reply("\u{1B}[31m\(text)\u{1B}[0m")
}
func toGreenString(_ text: String, withReply reply: @escaping (String) -> Void) {
reply("\u{1B}[32m\(text)\u{1B}[0m")
}
}
Alright, that's it. With that, we can build our agent code:
1
$ swift build
That should have build without a problem. Let's now register our agent. If you remember we will need a plist file in ~/Library/LaunchAgents/
to register our agent. Create a new file inside ~/Library/LaunchAgents/
named com.rderik.rdconsolesequencerxpc.plist
with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>com.rderik.ServiceProviderXPC</string>
<key>Program</key>
<string>PATH_TO_YOUR_PROJECT/rdConsoleSequencer/.build/debug/rdConsoleSequencer</string>
<key>MachServices</key>
<dict>
<key>com.rderik.ConsoleSequencerXPC</key>
<true/>
</dict>
</dict>
</plist>
The format should be familiar to you after seeing the one we created for the script to monitor the size of a directory. But there are some differences and one field you need to fix. Let's see the differences:
- Mach Service field on the Plist defines the Mach services exposed by the agent. The name of the service is the same name we registered when creating the listener in the
main.swift
:
1
let listener = NSXPCListener(machServiceName: "com.rderik.ConsoleSequencerXPC" )
We don't have an interval field or the run at launch field because our Mach Service will be loaded on-demand when a request reaches launchd
.
Ok let's load the agent:
1
$ launchctl load ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
And we can check it is running with the following command:
1
$ launchctl list | grep rderik
You'll see something like this:
1
- 0 com.rderik.ServiceProviderXPC
The name of the service is the one we register as the Label in com.rderik.rdconsolesequencerxpc.plist
.
Ok, the agent is running we now need to test it by creating a tool that uses it.
Consuming our XPC service
We are going to create another Swift executable using SPM(Swift Package Manager), to consume our service. The executable will be a simple one. It'll be a REPL that will read what the user types then echo it back in red and green.
Ok, let's create another directory for our new application. Name it rdConsoleSequencerClient
and initialise it with SPM:
1
2
3
$ mkdir rdConsoleSequencerClient
$ cd rdConsoleSequencerClient
$ swift package init --type executable
Remember the workflow to consume an XPC service:
(1) We create a connection to the service we want to use. The service is looked up by name (the Mach service we register for our agent). (2) Set the remote object Interface, here we need to know the protocol that describes the interface, the one we used to create the service on our agent. (3) Get an instance of the object that implements the interface (using remoteObjectProxy). (4) And last, make use of the service. Remember that the calls are always asynchronous, that means if we are going to work with the UI, we need to send it through the main Queue, so we don’t block the main thread.
Ok, let's edit our main.swift
and add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
print("Welcome to our simple REPL")
let connection = NSXPCConnection(machServiceName: "com.rderik.ConsoleSequencerXPC")
connection.remoteObjectInterface = NSXPCInterface(with: ConsoleSequencerXPCProtocol.self)
connection.resume()
let service = connection.remoteObjectProxyWithErrorHandler { error in
print("Received error:", error)
} as? ConsoleSequencerXPCProtocol
while true {
print("Insert text: ", terminator: "")
let text = readLine(strippingNewline: true)!
service!.toRedString(text) { (texto) in
print(texto, terminator: " ")
}
service!.toGreenString(text) { (texto) in
print(texto)
}
if text == "exit" {
break
}
}
Notice how we create the connection to the Mach Service with the name: com.rderik.ConsoleSequencerXPC
, the same we register in our Agent.
We also need to create the ConsoleSequencerXPCProtocol.swift
file. You can copy it from the agent to the Source directory. Here is the code:
1
2
3
4
5
6
import Foundation
@objc(ConsoleSequencerXPCProtocol) protocol ConsoleSequencerXPCProtocol {
func toRedString(_ text: String, withReply reply: @escaping (String) -> Void)
func toGreenString(_ text: String, withReply reply: @escaping (String) -> Void)
}
Ok, let's make sure our agent is running:
1
$ launchctl list | grep rderik
If we don't have it loaded, lets add it:
1
$ launchctl load ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
And now let's build and run our Client:
1
$ swift run
You might be prompted to give permission to the Agent, allow it to access your documents, this is because it needs to access the executable file.
Now when you type something you get it back in red and then in green colours.
Congratulations, your XPC service is working.
Cleanup
After we are done playing around with the XPC service, we should deregister the service and remove it form our ~/Library/LaunchAgents/
directory.
First, let's deregister it using launchctl
:
1
$ launchctl unload ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
Make sure is no longer register:
1
$ launchctl list | grep rderik
Now we can safely remove it from ~/Library/LaunchAgents
:
1
$ rm ~/Library/LaunchAgents/com.rderik.rdconsolesequencerxpc.plist
And that's it, back to a clean state.
Final thoughts
Now you know how to create Launch Agents, and also how to provide XPC services through your Launch Agents.
XPC services are a useful mechanism to communicate your applications. Now you create a service that interacts with your main application and have the Agent also be a bridge between other applications and your main application. You'll have to work with entitlements if you are distributing your application through the App Store. But you can test it in development by adding a temporary entitlement:
1
com.apple.security.temporary-exception.mach-lookup.global-name
It is an array, where you add the XPC services that your application can connect to. For our example, the array will contain the following item: com.rderik.ConsoleSequencerXPC
.
I hope that the two posts have been helpful. Now you know how to create XPC Service bundles in your applications and how to provide XPC services in your Launch Agents or daemons.
Let me know other ways that you are using XPC. I like to learn from what others are doing.
Ok, until next time.
Related topics/notes of interest
- Link to the post that describes how to create XPC Service bundles and explains the basics of XPC I encourage to read it if you haven't already.
- Apple's documentation no Creating XPC Services, also, Daemons and Services Programming Guide
- A handy site describing
launchd
and the plist configuration - Wikipedia's entry on launchd
- ANSI/VT100 Terminal Control Escape Sequences
- A more detailed Control Escape Sequences resource from docs.microsoft.com