OS X: How To Perform an Action During Fast User Switch

Becoming a FAQ: "How do I [perform some action] when fast user switching is invoked?" Alternatively, the question is "How do I perform some action when my IP address changes?" or some similar query. Brace yourself: it is possible, but there's no GUI for it. You have to dive for a shell.

OS X fires events just about anytime things change. Better still, is that you can run scripts based on these change events. It's the System Configuration framework that is responsible for this, and it runs as a daemon named configd. configd is what is responsible for setting up new resolvers (DNS servers) when your network changes. configd is the reason your changes made with ifconfig don't stick. The end-user's interface to configd's database is the underwhemingly documented scutil. That's about to change.

As with all good things, fire up Terminal and sudo or su up to root. Type 'scutil' and press return. The prompt will change to indicate that you're in scutil command mode. Type 'list' and press return. You'll get something like you see here, in Figure 1:

Fus-Scutil List

Figure 1: Listing the keys in configd

Each one of those keys lists an item that configd tracks. The 'State' variables are the items that track the current state of configuration - an Ethernet port, power state, etc. If you look towards the bottom of the list, you'll notice "State:/Users/ConsoleUser". This is they key that changes when a GUI user changes for any reason logs in, logs out or due to fast user switching. You can type "quit" now and press return to quit our of scutil. (Keep root, though).

So, what can we do with this? Turns out, there's a xml file (surprise!) that the System Configuration framework uses to keep track of what to to when the State settings change. You'll find it at:

/System/Library/SystemConfiguration/Kicker.bundle/Contents/Resources/Kicker.xml

Take a look at this file with your favorite text editor. You'll see a number of entries, all inside dictionary containers. You'll see settings that look like the scutil list - DNS, IPv4, etc. Well, we can add our own. Let's make a simple shell script:
#!/bin/bash

logger -p local0.notice -i -t cuctest Starting CUC Test now!

This will write an entry into /var/log/system.log when invoked. Save it as cuctest.sh (Console User Change test), mark it executable (chmod 770 cuctest.sh) and try it! Check your system log:

Fus-Cuctest-1

Figure 2: Running the test script to write into system.log (via syslog)

Great - we know it'll run. Now, let's hook it into configd. Add this to /System/Library/SystemConfiguration/Kicker.bundle/Contents/Resources/Kicker.xml:

<dict>
<key>execCommand</key>
<string>/var/root/bin/cuctest.sh</string>
<key>execUID</key>
<integer>0</integer>
<key>keys</key>
<array>
<string>State:/Users/ConsoleUser</string>
</array>
<key>name</key>
<string>CUCTest</string>
</dict>

(You can see that I saved my script under a "bin" directory in root's home - point the execCommand string at the appropriate place in your setup). The "execUID" property tells configd which user to run this script as. In my case, I'm playing fast and loose and letting it run as root. In some cases, that's appropriate, in others, you may want a script to run as a particular user - you decide which is the right action for your circumstance.

So, you can see we're watching the ConsoleUser state, and will execute /var/root/bin/cuctest.sh when it changes. You'll have to restart configd for it to pick up our change:

Jack-Kerouak:~/bin root# killall configd
Jack-Kerouak:~/bin root# configd
Jack-Kerouak:~/bin root# ps ax | grep [c]onfigd
5209 ?? Ss 0:00.96 configd


This is one of the reasons we stayed root, by the way. Now, we can test it! Go on and fast user switch, preferably to an account that can read /var/log/syslog. If you can't, just switch and then switch back. When I switch to another user, and look at /var/log/system.log, I see an entry like this:

Fus-Action

Figure 3: System log showing our script execute.

Note that configd will write into the log, too, to tell us which script is executing.

There you have it! Not too painful, right? Of course, this can get much more sophisticated via the events you choose to watch, and what you choose to do with your script. You may want to write a script that does different things for different users. Of course, you may also want to detect when a network change event occurs the network you're on via the State:/Network/Interface/blah property, figure out where you are, and set certain values appropriately (like default printer, volume, etc...just like OS 9!). Note, from the DNS/IPv4/IPv6 example in the file that you can watch multiple keys at the same time to kick off an event.

Just to complete this intro to scutil, note that you can do even more in the command mode. Get yourself back into scutil (you left your shell open, right?), and type list again. Let's examine some of the keys in the database. Understand that this database is kept up to date all the time by configd, so, it will change as your system changes. Pick a key - I'm going to choose State:/IOKit/PowerSources/InternalBattery-0, because I happen to be running on battery right now, and it makes for a good demo. We can get the state of the battery, or any key, with get keyname. Figure 4 shows the results of running get State:/IOKit/PowerSources/InternalBattery-0:

Fus-Scutilcm

Figure 4: checking the state of the internal battery

To display the contents of the key, show the dictionary with d.show. We can watch it, also. Add it to the watch array with n.add, and then watch it with n.watch. Watch:

Fus-Scutil Watch1

Figure 5: Watching configd's backing store change in real time.

If you're not a laptop/battery user and want to try something, try this:

Add a watch on IPv4:
> n.add State:/Network/Global/IPv4

Watch it:
> n.watch

Turn off Airport or unplug Ethernet:
> notification callback (store address = 0x302740).
changed key [0] = State:/Network/Global/IPv4

Plug it back in, turn Airport back on:
notification callback (store address = 0x302740).
changed key [0] = State:/Network/Global/IPv4

See what values are stuffed in there:
> get State:/Network/Global/IPv4
> d.show
<dictionary> {
PrimaryService : 2BFCF0BD-4183-4AB2-83B2-7B6651C77D45
Router : 192.168.100.1
PrimaryInterface : en1
}
scutil is used in a number of other ways, too. Remember that an OS X machine has three names: its DNS hostname, its computer name and its Bonjour name. scutil can be used to get or set any of these names (however, scutil is not really the right way to set these names, as they only affect the dynamic store, and won't survive a reboot! The proper tool is typically the GUI, systemsetup or networksetup):

scutil --get ComputerName
scutil --get HostName
scutil --get LocalHostName

Finally, scutil can tell you if a remote host is reachable:

Fus-Scutil Reach

Figure 6: scutil with the -r switch checks for reachability of a host.

While there is a little more that you can do with scutil, I've answered the question. Hope it helps you.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Thanks!

Amazing to me, how complicated things seem.

Thanks for the tutorial, what a great explanation of what is going on under the hood.