Issues with LaunchAgent and LaunchDaemon

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?

Answered by DTS Engineer in 801610022
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 with KeepAlive enabled

  • A launchd agent with KeepAlive enabled and LimitLoadToSessionType set to Aqua and LoginWindow

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.

All three processes must not restart if the user quits them

It’s impossible to achieve this goal for a launchd agent. That’s because:

  • A launchd agent must necessarily be running on the user associated with its session, otherwise it won’t be able to do agent-y things.

  • If it is, the user can kill it.

The only solution here is to structure your product so that:

  • The core functionality lives in the daemon, which the user can’t kill [1].

  • The user doesn’t gain anything by killing your agent.

I end up with two Agents, one owned by the user (the one I expect) and one owned by root.

It’s not clear how that comes about. One obvious way this can happen is if your agent loads into the pre-login session. However, you haven’t listed LoginWindow in your LimitLoadToSessionType. However^2, you earlier said that “They need to run during the login window”, which suggests that you should be listing it there. So I’m a bit confused.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Unless they’re as an admin user, and you can’t protect yourself from admin users.

Thank you for your response and apologies for taking so long to answer.

The only solution here is to structure your product so that: The core functionality lives in the daemon, which the user can’t kill [1]. The user doesn’t gain anything by killing your agent. BlockQuote

So essentially, my LaunchDaemon (in charge of the networking) and my LaunchAgent (in charge or capture SytemEvents) will always be running.

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 with KeepAlive enabled

  • A launchd agent with KeepAlive enabled and LimitLoadToSessionType set to Aqua and LoginWindow

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.

Issues with LaunchAgent and LaunchDaemon
 
 
Q