Mar 13

Using SystemConfiguration events within Python

Since this post was originally written, I've been working on the PyMacAdmin project with Nigel Kersten. The information below is still correct but the kicker-replacement script has gained the ability to handle filesystem events and workspace notifications and been renamed to crankd.

In a perfect world software would gracefully network transitions. Unfortunately my users have encountered a fair number of things which don't always handle things like a laptop moving from ethernet to WiFi, a DHCP server taking awhile to respond, etc. While many programs have at least reached the point of eventually timing out and retrying it would be nice to automatically restart something as soon as the system network configuration changes. This is unfortunately system-specific and frequently required some hackish approach involving tail -f or equivalent to watch a log file, which is slow and tends to break on upgrades.

OS X has a nice way to query the current system configuration and receive event notifications when things change: the SystemConfiguration Framework (Technical Note TN1145: Living in a Dynamic TCP/IP Environment is also of interest). You can explore this using the scutil command-line tool - in the example below, I've looked at the list of available events and chosen to watch for power-state changes, receiving a notice when I unplugged the power cable from my laptop:

chris@Enceladus:~ $ scutil
> list
 subKey [0] = Plugin:IPConfiguration
 subKey [1] = Plugin:InterfaceNamer
 subKey [2] = Setup:
 subKey [3] = Setup:/
 subKey [4] = Setup:/Network/Global/IPv4
 subKey [5] = Setup:/Network/HostNames
…
 subKey [21] = State:/IOKit/PowerManagement/CurrentSettings
 subKey [22] = State:/IOKit/PowerSources/InternalBattery-0
…
> n.add State:/IOKit/PowerSources/InternalBattery-0
> n.watch
> notification callback (store address = 0x1036c0).
 changed key [0] = State:/IOKit/PowerSources/InternalBattery-0
notification callback (store address = 0x1036c0).
 changed key [0] = State:/IOKit/PowerSources/InternalBattery-0

This is pretty cool stuff but I'd like to do something smarter than scripting a copy of scutil. I could write an Objective-C application but OS X 10.5 included the very handy PyObjC 2.0 which allows access to most of the native APIs directly from within Python. James Reynolds posted a message to the MacEnterprise mailing list which prompted me to stop procrastinating and actually write some code. A little poking around later and I have a Python script which is ready for me to add whatever custom actions I want to take when the network state changes - the version below is abbreviated so you'll want to download the full watch-network-config.py for your own use:

from Cocoa import*from SystemConfiguration import*defhandleNetworkConfigChange(store,changedKeys,info):print"Global network configuration changed: ", changedKeys
	# Kick a change-intolerant service in the head here

store =SCDynamicStoreCreate(None, "global-network-watcher", handleNetworkConfigChange, None)SCDynamicStoreSetNotificationKeys(store, None, ['State:/Network/Global/IPv4','State:/Network/Global/IPv6'])CFRunLoopAddSource(CFRunLoopGetCurrent(), SCDynamicStoreCreateRunLoopSource(None, store, 0), kCFRunLoopCommonModes)CFRunLoopRun()

Geoff Franks took the time to have the event handler use a dictionary so you can listen for multiple events and run a specific command for each one; I added a little syslog support and am releasing this version as a replacement for the widely-used Kicker which was removed in 10.5: Download kicker-replacement

Fun lessons from the trenches: in versions of OS X prior to 10.5 there were several nasty bugs due to lookupd and DirectoryService not having real timeouts: we have some rigs which use DHCP on our public network and static IPs on a private experiment network. When the system booted the private interface didn't need to wait for a DHCP lease and thus came up slightly faster than the public interface — this should have been harmless except that DirectoryService immediately attempted to connect to our LDAP server which isn't reachable on the private network and network timeout values aren't actually used in any version prior to 10.5, causing network accounts and NFS mounts to be unavailable until someone manually killed DirectoryService!