Saturday, October 07, 2006

At the end of this post I mention that I'd like to find a way to start a process in the console user's session, from a root process in the startup item context. I do not know of a documented way to do this, but to be clear, there is currently (at least as of 10.4.8) a way to do it simply using LaunchServices.

LaunchServices is the recommended way to launch an application on OS X. When you double-click an item in the dock, it's launched using LaunchServices. When you double-click on a PDF file, it is LaunchServices that figures out that Preview.app is the best candidate to handle that file type (because you certainly don't have Adobe Acrobat installed), it opens Preview.app and tells it to open said file.

LaunchServices is a sub-framework the ApplicationServices umbrella framework, which is not daemon safe according to TN2083. "Daemon safe" means that daemon processes running in the root bootstrap context (aka startup item context) are allowed to link with and use the framework. One reason a framework would not be daemon safe, is if it uses the WindowServer process.

The WindowServer is not only in charge of managing windows on screen, it's also intimately involved with process management (with some help from the loginwindow process). As the matter a fact, when you launch an application it will ultimately be the WindowServer that does a fork() and exec() to start your process. You can use ps to see that the WindowServer is indeed the parent for most of your processes.

(Note that if you look at the Parent Process field in Activity Monitor, it will incorrectly report the Dock as the parent of processes that were launched by clicking their dock icon. This is incorrect, and can be verified by look at the output with ps.)

OK, back on topic. So we know the WindowServer is important to launching processes. Actually when we launch a process in typical Cocoa fashion using the NSWorkspace class, LaunchServices is invoked behind the scenes to send a message to the WindowServer via mach messages, requesting that the WindowServer fork() up a new process in the console user's session. So in order to use LaunchServices (or NSWorkspace), we need to be able to communicate with the WindowServer, which is the reason why LaunchServices (and therefore ApplicationServices and therefore Cocoa) is not "daemon safe".

However, as TN2083 points out, there is a "global window server" reference available in the startup item context. This window server reference appears to be an artifact from the past as Quinn explains in the Technote:

The reasons for this non-obvious behavior are lost in the depths of history. However, the fact that this works at all is pretty much irrelevant because there are important caveats that prevent it from being truly useful.

But the fact remains that there is a reference, and we can see it with the Bootstrap dump command.

However, the only users allowed to connect to the WindowServer process are root and the console user. (The console user is the "current" user in a fast-user-switched environment. The console user is the owner of the /dev/console device.) But if we're running as root we should be able to connect to the window server using standard LaunchServices calls. Let's try.

This will start TextEdit in my current user session. We can use Bootstrap dump again to verify that the TextEdit process is indeed running in my session, but it is. The command /usr/bin/open links against Cocoa, and works the same way, so I can simply type sudo /usr/libexec/StartupItemContext /usr/bin/open -a TextEdit to do the same thing.

As I mentioned above, only the console user and root are allowed to connect to the WindowServer process, so if your daemon is running as nobody (for example), it won't be able to do this.

(The first command above starts a shell running as user nobody in the startup item context.)

So, we can see that from the startup item context, as root, we are able to launch a process in the console user's session, simply using normal LaunchServices calls. But note that this is undocumented behavior, and it is not guaranteed to work at any point in the future. It merely worked in this example.

First off, I'm not sure, but I'll venture a guess anyway. Given that LaunchServices will start a process in a user's session, I'd think that any applications would work since they're actually running in a user session and would have access to all the services available to that user. A quick test with the Google Notifier, which is an LSUIElement, seems to work:sudo /usr/libexec/StartupItemContext /usr/bin/open -a "Google Notifier"

The global window server connection trick is what allows ssh session to use open to run applications and osascript to control them (I do it all the time for MAMP or similar).

Anyway, the blessed, Apple-guaranteed-to-always-work way of doing things is having an agent running in the session that communicates in an Apple-sanctioned way (ie UNIX-domain sockets) with the daemon. The agent should then do the launching.

"The WindowServer is not only in charge of managing windows on screen, it's also intimately involved with process management"... "You can use ps to see that the WindowServer is indeed the parent for most of your processes"Hmmm a strong deja vu for Explorer.exe on a different OS... ;)