Keep app "alive" when going from Login to user context.

I have a remote control application working in user context but I'm trying to implement "remote login". During the LoginWindow I'm able to "launch" the app, establish connection and login but as son as I click log in the connection is lost until the app gets relaunched in the user context.

I'm reading about LaunchAgents, daemons and xpc services but it is all very confusing so I'm trying to get some pointers.

Right now I save my plist in /Library/LaunchAgents with the key LimitLoadSessionType set to LoginWindow "pointing" to my apps executable. This gets my app running during the LoginWindow and I can remote login.

Once I hit enter to login my app gets shutdown with no mercy until it gets launched in the user context by another LaunchAgent.

I know that there is a way to keep my app alive ( I say alive but I'm not sure that's how it actually works) while it goes from Login to user context. Does anyone know the work flow for a process like this? how does a LaunchAgent daemon connects to a xpc service?

I know that there is a way to keep my app alive … while it goes from Login to user context.

That’s not feasible. The pre-login session goes away when the user logs in and it takes all of the code running in that session with it.

Does anyone know the work flow for a process like this?

The standard pattern is have a launchd daemon that manages the network connection and then have a launchd agent that’s set up to both pre-login (LoginWindow) and GUI login (Aqua) sessions. The agent is instantiated for each relevant session — remember that you can have multiple GUI login sessions running simultaneously — and each instance connects to the daemon via an IPC mechanism of your choice (although I generally recommend XPC) and do work on behalf of the daemon in each GUI context.

ps A good place to start here is Technote 2083 Daemons and Agents. While a lot of minor details have changed over the years, the core concepts it explains are still relevant.

Share and Enjoy

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

Thank you for the prompt response! I have some more questions if you don't mind.

The agent is instantiated for each relevant session — remember that you can have multiple GUI login sessions running simultaneously — and each instance connects to the daemon via an IPC mechanism of your choice (although I generally recommend XPC) and do work on behalf of the daemon in each GUI context.

Does this mean that in general my application should be done in two parts, and xpc service that deals with the networking, that can run without interruption in any session (login and logged in user), and then my NSApplication to deal with the very little UI I need and to capture user screen and other input?

The standard pattern is have a launchd daemon that manages the network connection and then have a launchd agent that’s set up to both pre-login (LoginWindow) and GUI login (Aqua) sessions.

As I mentioned before, it is a remote control application I'm working on and this is how I understand the standard patter would work.

my daemon:

  • It would be launched and kept alive through any session (LoginWindow and when user is logged in)
  • it would be in charge of my networking code, connecting to my client and receiving input from the client

may agent

  • one for LoginWindow and one when the user is launched.
  • takes care of the NSMenu my application represent
  • it captures all the screen, user input and sends it to my daemon which will then send it to the client.

Right now my application works and it is all contained within the same project, but it sounds like it would be better to split it and have a xpc service and a agent? this way The XPC Service can run and maintain connection with the client "non stop"?

Accepted Answer

Does this mean that in general my application should be done in two parts

I recommend three parts:

  • A launchd daemon for managing the network and centralised command and control.

  • A launchd agent running in each GUI login session, providing window server access on behalf of the daemon.

  • An app, that the user can run to configure the service.

Don’t try to merge the last two; that’s likely to end badly.

I was just reading about xpc service

You are confusing an XPC service with a named XPC endpoint. An XPC service is a bundled chunk of code that you can embed within your app. A named XPC endpoint is an XPC listener that you can look up by name and communicate with. An XPC service publishes a named XPC endpoint, but other code can as well. Specifically launchd daemons and agents can do this using the MachServices property in their launchd.plist.

An XPC service is unlikely to be helpful in this scenario.

Share and Enjoy

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

Hello Eskimo! I have a few more questions. I was looking for some steps or more info on how everything would work, mainly the LaunchDaemon. This is how I understand things as of right now.

LaunchDaemon - daemon for managing the network and centralised command and control. It is a remote control application so this means that the daemon will not get shut down and I can keep the connection to my client alive when moving from login to user context?

  • LaunchDaemon - launches service in and stays alive when going from login context to user context?
  • Once in user context, daemon can launch the app to provide ui to configure the service.

I was trying to start my service executable through my plist saved in LaunchDaemons (same plist as when I was starting the app from LanchAgents) but that does not work.

Is there any specific requirements for the service executable? My end goal is to provide a smooth transition when going from login to user context, which means not shutting down my service in order to maintain the connection with my client.

Once in user context, daemon can launch the app to provide ui to configure the service.

You seem to have misunderstood something pretty critical here. A launchd daemon always runs in the global context. No matter what happens to user contexts, they come, they go, the launchd daemon stays in that global context.

Given that, a launchd daemon can’t “launch the app” because, if it tries to run another process, that process will also run in the global context, and apps can’t run in a global context.

The standard pattern in to use a launchd daemon and a launchd agent. The daemon runs all the time. The agent comes and goes as user contexts come and go. So, if you have no users logged in, there will be no instances of your agent. And if you two users logged in, there will be two, one on each login context.

Share and Enjoy

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

Thank you for clarifying that, it makes sense now.

As far as the launch daemon, I need it to run the code that connects to my client so I’m assuming this would be by compiling my code into an executable? If so, how would I go about embedding the libraries I need into the executable? This is probably a whole different topic but any pointers would be greatly appreciated.

Hello Eskimo!

So I was able to run my program as a daemon and it is keeping the connection with my client but now I'm getting some strange behavior with my screen capturing. When going into the LoginWindow the frame I get is just the background image of Ventura OS but I'm able to remote control the mouse and keyboard as expected so it kind of makes me feel like it has something to do with my AVCapture session and it running in a global context? Still trying to find out more on this but I figured I would update my status.

As far as the launch daemon, I need it to run the code that connects to my client … ?

It seems like you figured this out already but I want to clarify one point here. In the macOS architecture, clients connect to daemons, not the other way around. So if you meant “the code that manages the connection to my client” then that’s fine. But if you’re daemon is actively opening a connection to your client then we need to talk some more.

When going into the LoginWindow the frame I get is just the background image of Ventura OS

That sounds like a TCC issue. What does CGPreflightScreenCaptureAccess return?

Share and Enjoy

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

That sounds like a TCC issue. What does CGPreflightScreenCaptureAccess return?

that returns true and everything works as expected when capturing the screen until I log out. That's when the my frames stay in the background image of the OS and I'm unable to see the password textField but everything else continues to work (mouse and keyboard remote control).

I'm now trying to connect to my Daemon but I'm unable to do it. Here is how I'm testing. Here is my daemon 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.myCompany.myCompanyServer</string>
	<key>MachServices</key>
        <dict>
            <key>com.myCompany.myCompanyServer.xpc</key>
            <true/>
        </dict>
	<key>RunAtLoad</key>
	<true/>
	<key>ProgramArguments</key>
	<array>
		<string>path to my exec </string>
		<string>service</string>
	</array>
	<key>QueueDirectories</key>
	<array/>
	<key>Item 0</key>
	<string>path to queue directory</string>
</dict>
</plist>

At this point I'm certain my daemon is up a running. Here is what it'll be my agent running as root. For some reason my interruptionHandler always gets called right away.

final class DaemonConnection {
    private var machServiceName = "com.myCompany.myCompanyServer.xpc"
    private var daemonConnection: NSXPCConnection
    
    init() {
        daemonConnection = NSXPCConnection(machServiceName: machServiceName, options: [.privileged])
        daemonConnection.interruptionHandler = interruptionHandler
        daemonConnection.invalidationHandler = invalidationHandler
        daemonConnection.resume()
    }
    
    private func interruptionHandler() {
        print("daemon connection was interrrupted")
    }
    
    private func invalidationHandler() {
        print("daemon conenction was invalidated")
        
    }
}

and here is my daemon code:

private var machServiceName = "com.myCompany.myCompanyServer.xpc."
xpcListener = NSXPCListener(machServiceName: machServiceName)
xpcListener.delegate = self
xpcListener.resume()

then my listener delegate

func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        Logger.shared.logDebug(logtag, " ******* received new xpc connection request from \(String(describing: newConnection.serviceName)) *****")
        return true
 }

when I run launchctl print system/myDaemon.plist I see

environment = {
		XPC_SERVICE_NAME => com.myCompany.myCompanyServer
}

and I would expect that to be com.myCompany.myCompanyServer.xpc? so it seems I'm missing something? Let me know what you think

I’m going to recommend that you open a DTS tech support incident so that I can dedicate more time to helping you with this. I’ve worked with a number of developers in your situation. It’s always been possible to find a solution but getting all the details right is kinda tricky.

Please reference this DevForums thread for context.

Share and Enjoy

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

Hello! I crated the support incident and today as I was trying again and I realized my app was sandbox. After dealing with that this is what I have:

let service = daemonConnection.remoteObjectProxyWithErrorHandler { err in
   print("Received error: \(err.localizedDescription)")
} as? MyServiceProtocol

which prints:

Received error: Couldn’t communicate with a helper application.

or

Received error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.serviceName" UserInfo={NSDebugDescription=connection to service named com.serviceName }

And one more update, my NSXPC connection is now working. But I'm sill facing the same issue with the screen capturing. Everything works great until I log out, I get most of the image but I cannot see the textview to enter my password but everything else works (remote keyboard, mouse, etc) and the screen frame I get remains the same from then on (the cool red background image for Ventura)

Keep app "alive" when going from Login to user context.
 
 
Q