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.
$ ps jaxww | grep WindowServe[r]
windowse 59 1 59 .../CoreGraphics.framework/Resources/WindowServer -daemon
$ ps jx | awk '{if ($3 == 59) print}'
jgm 132 59 ... /System/Library/CoreServices/Dock.app/Contents/MacOS/Dock -psn_0_393217
jgm 134 59 ... /System/Library/CoreServices/SystemUIServer.app/Contents/MacOS/SystemUIServer -psn_0_524289
jgm 136 59 ... /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder -psn_0_655361
jgm 139 59 ... /Applications/Google Notifier.app/Contents/MacOS/Google Notifier -psn_0_786433
jgm 197 59 ... /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal -psn_0_1572865
jgm 257 59 ... /Applications/Safari.app/Contents/MacOS/Safari -psn_0_2490369
(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.
$ sudo /usr/libexec/StartupItemContext ~/bin/BootstrapDump | grep Window
"com.apple.windowserver" by "/System/Library/.../Resources/WindowServer"
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.
$ cat -n launch.m
1 #import <Cocoa/Cocoa.h>
2 int main(void) {
3 [[NSWorkspace sharedWorkspace]
4 launchApplication:@"TextEdit"];
5 return 0;
6 }
$ gcc -o launch launch.m -framework Cocoa
$ sudo /usr/libexec/StartupItemContext ./launch
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.
$ sudo /usr/libexec/StartupItemContext /usr/bin/sudo -u nobody -s
$ open -a TextEdit
kCGErrorRangeCheck : Window Server communications from outside of session allowed for root and console user only
INIT_Processeses(), could not establish the default connection to the WindowServer.Abort trap
(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.