Wednesday, April 15, 2009

Getting the ByHost string

Some preferences are per-computer, and Apple stores these preferences in "ByHost" folders in both /Library/Preferences and ~/Library/Preferences. Additionally a string that is supposed to uniquely identify the computer is added to the "preference domain" (the application's identification string in reverse-DNS notation). So the ByHost preferences for iTunes on my computer is named "com.apple.iTunes.776E2F18-DBC3-5F9E-BF52-C003E6E6C160.plist". Finding the bit that goes before the ".plist" is the real trick, and the subject of this post.
Most programmers will ever need to get this information as they don't have anything that should or needs to be stored on a by-host basis. Most of the rest can use the CFPreferences API's to do so (by supplying the kCFPreferencesCurrentHost constant), thereby always having the right information.
However, system administrators sometimes need to manage this information. For simple key-value items that are in the root of the given plist this is easily covered by using the '-currentHost' flag with 'defaults' command. But if the plist you are managing has any arrays or dictionaries in it that you want to touch you are probably stuck needing the value.
Apple has already changed this system once: at one point the value you needed to use was always the MAC address of the primary network interface (en0) without periods and all lower case. However, at some point along the way Apple changed their mind and now use a longer UUID formated string. But, to preserve compatibility, older hardware still uses the older value and newer hardware uses the new format. So you have to figure out what case you are in.
The value is stored in the IORegistry, and this is relatively easy to get ahold of either by scripting or in C. For scripting it is probably easiest to shell out and use the 'ioreg' command. Then you need to decide if you have an old-style machine, or one that uses the new style. This turns out to be easy because Apple prepends "00000000-0000-1000-8000-" to the MAC address for older computers. I have modified the script snippit that you can find on AFP548 a little and present this version (in bash form):
PLATFORM_UUID=`/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice | /usr/bin/awk '/IOPlatformUUID/ && gsub("\"", "") { print $3 }'`
if [[ "$PLATFORM_UUID" == 00000000-0000-1000-8000-* ]]; then
 PLATFORM_UUID=`echo "$PLATFORM_UUID" | /usr/bin/awk 'BEGIN { FS = "-" }; { print tolower($5) }'`
fi
I do have some concern about the output of ioreg could change at some point, breaking this script, but that is something you always have to worry about when using shell commands in  scripting. The Foundation/Cocoa way of doing this does not suffer from this, but you do have to dive into the IOKit to do so (there is no Obj-C way of doing this, so you have to use this snippit of C code):
NSString * byHostIDString = nil;
io_struct_inband_t iokit_entry;
uint32_t bufferSize = 4096; // this signals the longest entry we will take
io_registry_entry_t ioRegistryRoot = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/");
IORegistryEntryGetProperty(ioRegistryRoot, kIOPlatformUUIDKey, iokit_entry, &bufferSize);
byHostIDString = [NSString stringWithCString:iokit_entry encoding:NSASCIIStringEncoding];
// older comptuers use only the mac address portion of the UUID string
if ([byHostIDString hasPrefix:@"00000000-0000-1000-8000-"]) {
 NSArray * UUIDElements = [byHostIDString componentsSeparatedByString:@"-"];
 byHostIDString = [(NSString *)[UUIDElements objectAtIndex:[UUIDElements count] -1] lowercaseString];
}
IOObjectRelease((unsigned int) iokit_entry);
IOObjectRelease(ioRegistryRoot);
You will need to include Foundation and IOKit in your project, and if you are actually using this code you might want to put a few more checks in there to make sure that you don't get NULL or nil back from anything. I have never seem those code paths active on my implementations.
I don't know who first figured out the scripting method of figuring out the ByHost key, but I have been using AFP548's article on the subject as my reference on this one for a while. There are some important notes in the comments, so read more than just the article.

Tuesday, April 14, 2009

Improving on NSLog

While developing I am a big user of logging. Having a well-formated series of reports from my half-developed program telling me that everything has worked up to that point is very comforting to me. It is also really nice when something goes horribly wrong to be able to dial-up the logging on a program or script to help figure out what is going on. But when running a program day-to-day I don't want all the noise clogging up my system logs.
Up until very recently I have been using mixtures of NSLog, printf/fprintf, and syslog to get my debugging logged, but each one of those has their limitations: NSLog does not have any sense of "log level", the printf/fprintf combination gives me two levels of logging, but I then have to manage all of that and figure out where to store things, and syslog mostly matches my thinking but does not integrate completely into a Cocoa program.
Then I read Peter Hosey's series of posts about ASL (Apple System Logger). ASL does have glaring hole for Cocoa programming: it does not understand NSStrings. But that same series of posts had the solution to that: use a #define statement to convert the format and arguments to a single UTF string, which ASL is very happy to take. He even provided a line to do so: 
#define asl_NSLog(client, msg, level, format, ...) asl_log(client, msg, level, "%s", [[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String])
This was almost enough, but there are still a few things I was not happy with: I only use the default client and message, and there are times I want to have the debugging output to go to stdout/stderr in addition to the system logs. So I have modified Peter's example and come up with the following:
// Inspired by Peter Hosey's series on asl_log
#ifndef ASL_KEY_FACILITY
 #define ASL_KEY_FACILITY "Facility"
#endif

#define LOG_LEVEL_TO_PRINT ASL_LEVEL_WARNING
#define NSLog_level(log_level, format, ...) asl_log(NULL, NULL, log_level, "%s", [[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String]); if (log_level >= LOG_LEVEL_TO_PRINT) { if (log_level "%s\n", [[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String]); } else { printf("%s\n", [[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String]); } }

#define NSLog_emerg(format, ...) NSLog_level(ASL_LEVEL_EMERG, format, ##__VA_ARGS__)
#define NSLog_alert(format, ...) NSLog_level(ASL_LEVEL_ALERT, format, ##__VA_ARGS__)
#define NSLog_crit(format, ...) NSLog_level(ASL_LEVEL_CRIT, format, ##__VA_ARGS__)
#define NSLog_error(format, ...) NSLog_level(ASL_LEVEL_ERR, format, ##__VA_ARGS__)
#define NSLog_warn(format, ...) NSLog_level(ASL_LEVEL_WARNING, format, ##__VA_ARGS__)
#define NSLog_notice(format, ...) NSLog_level(ASL_LEVEL_NOTICE, format, ##__VA_ARGS__)
#define NSLog_info(format, ...) NSLog_level(ASL_LEVEL_INFO, format, ##__VA_ARGS__)
#define NSLog_debug(format, ...) NSLog_level(ASL_LEVEL_DEBUG, format, ##__VA_ARGS__)
Now I have 8 functions that I can use as drop-in replacements for NSLog that will send the output to the system logging facility, and depending on the level I set with LOG_LEVEL_TO_PRINT, also to stdout/stderr. For me this also has the nice benefit of getting rid of all of the timestamps in the XCode console when I am developing which helps me concentrate on my program's output better (while still preserving this information in the system log).

About this Blog

As an Mac administrator I have benefited greatly from the writings of other Mac administrators such as Greg Neagle and those who have contributed so much to MacEnterprise and AFP548. So I would like to contribute some of my own content.
I have already done so in a few cases, and will try to continue to do so on mailing lists and forums, but I would like to have something at the end of the day that I can point to and call mine. This blog hopefully will also allow me to publish smaller hints that would not be appropriate for full posts on the larger sites. The other main benefit of doing this on a site I can control is that I can go back and revise things as they change.
My job leads me to do a lot of things that straddle the boarder between scripting and programming, so the posts will probably reflect that mixture.