I have this application that is divided in 3 parts.
- A server that handles all the networking code
- A agent that handles all System related code
- A manager (NSApplication) to interact with the other two processes.
Goals
- All three process should be kept alive if they crash
- All three processes must not restart if the user quits them though the NSApplication
- They need to run during the login window.
My current set up using LaunchD is as follows.
My Server process plist (relevant part) saved in System/LaunchDaemons
key>MachServices</key>
<dict>
<key>com.myCompany.Agent.xpc</key>
<true/>
<key>com.myCompany.Manager.xpc</key>
<true/>
</dict>
<key>ProgramArguments</key>
<array>
<string>PathToExecutable</string>
<string>-service</string>
</array>
<key>RunAtLoad</key>
<false/>
My agent plist (saved in System/LaunchAgent)
<key>QueueDirectories</key>
<array>
<string>PathToDirectory</string>
</array>
<key>RunAtLoad</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>PathToExecutable</string>
<string>service</string>
</array>
my Manager app plist (saved in System/LaunchAgent)
<key>LimitLoadToSessionType</key>
<string>Aqua</string>
<key>RunAtLoad</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>PathToExecutable</string>
</array>
<key>MachServices</key>
<dict>
<key>com.myCompany.Agent.manager.xpc</key>
<true/>
</dict>
Currently I have another app that saves a file to the path of the QueueDirectories which triggers the launch of my Agent which then triggers the Server and Manager by starting a XPC Connection. QueueDirectories keeps the Agent alive (and hence all other processes) til file is removed and processes are quited through the manager.
XPC Connections
- Server listens for a connection from agent and manager
- Manager listens for a connection from agent and starts a connection with server
- Agent starts a connection with Manager and Server
Agent and Manager are owned by the user and server by root
The problems
When I start Agent by saving a file in the QueueDirectories path and connect to the Server over xpc I end up with two Agents, one owned by the user (the one I expect) and one owned by root.
But if I manually start the Agent I do not have that problem.
As I mentioned before, the server listens for a connection from the Agent.
How do I stop getting two instances? or what is a better way to approach this?
my LaunchDaemon (in charge of the networking) and my LaunchAgent (in charge or capture SytemEvents) will always be running.
Yes, but it’s important that you understand the subtleties here.
You can think [1] of a launchd
job (a daemon or an agent) as being in one of three states:
-
Unloaded — The session doesn’t know about the job at all.
-
Loaded — The session knows about the job but hasn’t started its process.
-
Started — The session knows about the job and has started its process.
The job goes from the Loaded to Started state based on demand. For example, if the job publishes a named XPC endpoint (via the MachServices
property) then a client connecting to that endpoint will start the job. In a case like this, where you want the process to run continuously, you synthesise that demand by setting the KeepAlive
property [2].
The next thing to consider is the session. A launchd
daemon is in the global session. That session is created when the system starts and exists until the system shuts down.
OTOH, launchd
agents exist in specific sessions. For example, a GUI login session is created when the user logs in and is destroyed when the user logs out. If you configure your agent to run in GUI login sessions, the system will create an instance of it in each such session. You do this by setting LimitLoadToSessionType
to Aqua
(or omitting that property, because Aqua
is the default).
IMPORTANT By default the agent defaults to the Loaded state. If you want the agent to start, you’ll need to either generate some demand or set KeepAlive
.
The LimitLoadToSessionType
property can hold multiple values. If you want to run in GU login sessions and pre-login sessions, set it to Aqua
and LoginWindow
. The pre-login session is created when the login window starts and terminates when the user logs in [3].
A typical setup here is:
-
A
launchd
daemon withKeepAlive
enabled -
A
launchd
agent withKeepAlive
enabled andLimitLoadToSessionType
set toAqua
andLoginWindow
The system will then:
-
Start the daemon on boot and keep it running continuously.
-
Start an instance of the agent for each pre-login session and each GUI login session.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] I’m glossing over a lot of details here, but this model is still useful.
[2] You can also use others properties, like RunAtLoad
and the deprecated OnDemand
.
[3] Or they abandon the login attempt, for example, during a fast user switching attempt.