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.

3 comments:

  1. byHostIDString = [(NSString *)[UUIDElements objectAtIndex:[UUIDElements count] -1] lowercaseString];

    should be written as

    byHostIDString = [(NSString *)[UUIDElements lastObject] lowercaseString];

    ReplyDelete
  2. Great.

    I was difficult to find a path to solve this on my app.

    Thanks a lot !

    Another issue I am dealing with is how to add, as a first bit on a string with this byHostIDString, a CRC check.

    Juan

    ReplyDelete
  3. An easier and more robust way of getting the platform uuid using PlistBuddy and a more succinct ioreg command in the shell:

    /usr/sbin/ioreg -a -p IODeviceTree -r -c IOPlatformExpertDevice -d 1 > /tmp/ioplatformexpertdevice.plist
    PLATFORM_UUID=$(/usr/libexec/PlistBuddy -c "Print 0:IOPlatformUUID" /tmp/ioplatformexpertdevice.plist | tr A-Z a-z)

    ReplyDelete