Friday, September 29, 2006

User Notification from the Startup Context

Mac OS X is a great Unix, but since it's a combination of Mach and BSD it has some parts that are new to many traditional Unix users. All Unix systems have the concept of sessions with regard to collections of processes (man getsid(2) for more details), but Mac OS X adds an additional, and very different, concept of a session.

For a quick introduction to sessions on Mac OS X, see tn2083, and for the best description see Amit Singh's Mac OS X Internals book (chapters 5 & 9). In a nutshell, when the system first boots it has one session -- the root session, or the startup context. All processes started in this session will themselves be in this session. Launchd, syslogd, configd, and all system daemons run in the startup context. launchd starts loginwindow.app to handle user logins, but the loginwindow app also creates a new session for each user as they log in. So, every user on the system runs in their own session, and every user's session is different from the startup context (a user's session is also different each time they log in).

Apple provides a sample program called BootstrapDump that will show you all the mach services that are visible in a given context. For example, we can download and compile BootstrapDump.c with:

$ curl -s http://developer.apple.com/samplecode/BootstrapDump/BootstrapDump.zip > tmp.zip
$ unzip -q tmp.zip
$ cd BootstrapDump/
$ gcc -o BootstrapDump BootstrapDump.c
$ ./BootstrapDump
...
"com.apple.PowerManagement.control"
"com.apple.SystemConfiguration.PPPController"
"com.apple.network.EAPOLController"
"com.apple.network.IPConfiguration"
"com.apple.windowserver.active"
...


This program is very useful for debugging and exploring the system. We can also see the difference in the services running in my login session vs the services available to the startup context.

$ ./BootstrapDump | wc -l
227
$ sudo /usr/libexec/StartupItemContext ./BootstrapDump | wc -l
42


Daemons are background processes, so they should very rarely need to display a notice to a user, but occasionally the need arises. Since only the user who's currently sitting at the console is allowed access (according to documentation) to the window server, how can daemons (or kernel extensions) notify the user of certain important events? This question really boils down to: how can something running in the startup context display a notification to the console user in the console user's context?

As it turns out, Apple has provided us with three solutions to this problem:

1) CFUserNotifications
2) Libunc
3) KUNC

The first two are very similar (almost copy-n-paste identical), and the third is built on the first one. I won't go into detail about using these APIs, rather I'll talk about how they work. See the available documentation and header files for usage details about the APIs.

These APIs are NOT intended for use in regular applications. Rather applications that the user launches should use normal Carbon/Cocoa methods to display windows or alerts (NSRunAlertPanel, etc).

CFUserNotifications



From the CFUserNotification.h header file:

A CFUserNotification is a notification intended to be presented to a user at the console (if one is present). This is for the use of processes that do not otherwise have user interfaces, but may need occasional interaction with a user.


One of the convenience functions available to work with CFUserNotifications is CFUserNotificationDisplayAlert(), which is a blocking call that simply displays an alert window on the console user's screen. If we look at the source, we can see that the function CFUserNotificationSendRequest() is eventually called and it works by sending a mach message to the mach port named "com.apple.UNCUserNotification". This service may then be responsible for displaying the message. If we look at this service with BootstrapDump we can see that the service indeed exists, and that messages sent to this service will cause the UserNotificationCenter.app application to be launched "on demand".

$ BootstrapDump | grep UserNo
"com.apple.UNCUserNotification" by "/System/Library/CoreServices/UserNotificationCenter.app/Contents/MacOS/UserNotificationCenter" on demand


Let's see all this in action with a test program:
$ cat -n cfu.m
1 #import <CoreFoundation/CoreFoundation.h>
2 int main(void) {
3 CFUserNotificationDisplayAlert(0, 0, NULL, NULL, NULL,
4 CFSTR("header"), CFSTR("message"), CFSTR("default button"),
5 CFSTR("alt button"), CFSTR("other button"), NULL);
6 return 0;
7 }
$ gcc -o cfu cfu.m -framework CoreFoundation
$ sudo /usr/libexec/StartupItemContext ./cfu


Then in another shell use ps and look for a process named UserNotificationCenter, and make note of its PID, then use BootstrapDump to see what its context looks like:

$ ps aux | grep UserNo[t]
jgm 740 0.0 -0.2 230132 4696 ?? Ss 4:39PM 0:00.22 .../Contents/MacOS/UserNotificationCenter
$ sudo BootstrapDump 740 | wc -l
233
$ BootstrapDump $$ | wc -l
233
$ diff <(sudo BootstrapDump 740 ) <(BootstrapDump $$)


We can see that the UserNotificationCenter process was indeed started on demand, and that its mach context looks just like mine. So we see that calling CFUserNotificationDisplayAlert() sends a mach message to the mach port named "com.apple.UNCUserNotification", which causes UserNotificationCenter.app to be launched on demand to handle the request (think of this as how inetd starts servers on demand).

The next question is, how does this work? And who's listening on the mach port named by "com.apple.UNCUserNotification" before UserNotificationCenter.app is launched on demand, i.e., who starts UserNotificationCenter.app? A little poking around on the system indicates that loginwindow.app knows about the "com.apple.UNCUserNotification" mach service, and it also knows the details of my mach session, so loginwindow.app is the most likely candidate.

$ cat /System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow | strings | grep UserNo
UNCUserNotification
com.apple.UNCUserNotification
/System/Library/CoreServices/UserNotificationCenter.app/Contents/MacOS/UserNotificationCenter


(See this post to see I use cat before the strings.)

So to recap the whole thing, a daemon process running in the startup context can create a CFUserNotification, which will send a mach message to "com.apple.UNCUserNotification", loginwindow.app will notice this and fork and exec UserNotificationCenter.app in the correct user session to actually handle displaying the user notification window.

Libunc - in libSystem



This is a system level re-implementation of the same functionality provided by CFUserNotification. Note that it does not use CFUserNotification to do its job, rather it's actually a reimplementation of it. The source is available here.

KUNC



As strange as it may seem to some, the situation may even arise where a kernel extension may need to display a notification to a user. And sure enough, Apple has provided us with this functionality too. The KUNC APIs can be used to do just this. Apple has some documentation about these APIs here, and the source is available as part of xnu at here.

Code running within the kernel may call an API, for example, KUNCUserNotificationDisplayNotice(), that will display a notification window on the console user's screen (and running in their context). This works very much like CFUserNotifications, and as the matter a fact, it is built on the CFUserNotification API. The main difference is that KUNC uses another userland daemon (/usr/libexec/kuncd) to transform KUNC API calls to CFUserNotification API calls. Presumably, this is done because kernel extensions can't link against CoreFoundation.

The kernel maintains a special mach port called the user notification port. This port can be get and set using the MIG calls host_set_UNDServer() and host_get_UNDServer(). When launchd is starting up upon boot, it runs the program /usr/libexec/register_mach_bootstrap_servers to register old-style mach servers defined in /etc/mach_init.d. One of these servers is /usr/libexec/kuncd (registered with /etc/mach_init.d/kuncd.plist). When this server is registered, register_mach_bootstrap_servers calls host_set_UNDServer() to set the host user notification port to a port that will start /usr/libexec/kuncd on demand. As we can see from BootstrapDump (and kuncd.plist) the service is named "com.apple.system.Kernel[UNC]Notifications".

$ BootstrapDump | grep kunc
"com.apple.system.Kernel[UNC]Notifications" by "/usr/libexec/kuncd" on demand


After this, KUNC APIs called from within the kernel (note: KUNC can only be called from within the kernel) send a message to the host user notification port, and /usr/libexec/kuncd is started on demand to handle the request. kuncd itself is started by launchd and runs in the startup item context, but it simply makes CFUserNotification calls which then handle the rest of the request exactly as described above.

KUNC actually has one more interesting function: KUNCExecute(), which takes a path to an executable, a UID, and a GID. It doesn't make much sense to have the kernel fork() and exec() like a normal process, so it's interesting to consider how this works. Basically, KUNCExecute() again sends a mach message to the host user notification port, /usr/libexec/kuncd answers that request, but this time CFUserNotification is not used. Rather the execution is handled by a call to _SCDPExecCommand() from the SystemConfiguration framework. This method simply does a fork(), setuid(), and execv(), of the binary. The key point here is that the program run with KUNCExecute() is NOT run in a user's login context, rather it's just run in the startup context like launchd and other daemons.


All of these notification methods can be visualized by considering this figure:

However, note that the libunc/libSystem approach mentioned above is not pictured. If it were, it would show up as another framework (library) in magenta similar to CoreFoundation in the figure.

I was recently asked how a daemon could display a notification to a user, and I wasn't sure. But after a little poking around, it appears that there's a few good options. I had also hoped to find a way to run a command as the console user, but I didn't find that. If someone knows how to do that, I'd love to know. Also, if anyone sees any inaccuracies in what I've written, please let me know.

[UPDATE: 10/7/2006:
Please see this post for more info about launching a process in a user context from the startup item context.]

2 comments:

Anonymous said...

The only thing i feel like missing a bit is the use of login hooks that will be run as root (cf. http://www.unix.com/showthread.php?t=30735 ). Similarly there also is the option to a 'add' a script to ~/Library/Preferences/com.apple.Terminal.plist (which can be converted to the XML plist format by man plutil; cf. http://www.askdavetaylor.com/binding_terminal_to_a_shell_script.html ).

And yet another topic that could get mentioned in the broader context is the possibility of Dynamically Overriding Mac OS X ( http://rentzsch.com/papers/overridingMacOSX ).

Just my two cents.

Cheers,
p.

Anonymous said...

Just in case someone is wondering how to get the current console user, there is an Apple doc dealing with this issue:

Determining user login/logout status

http://developer.apple.com/qa/qa2001/qa1133.html