Creating a Launch Agent that provides an XPC service on macOS using Swift Oct 21 2019

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

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:

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:

  1. Open System Preferences
  2. Go to Security and Privacy
  3. Scroll down to “Full Disk Access”
  4. Open the bin directory on Finder. From your terminal, you can use the open(1) command.

    1
    
    $ open /bin
    
  5. 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:

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.

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


** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.