About | Buy | Forum | Industry Watch | Learning Curve | Products | Search | Twitter | Xnews
Home » Learning Curve » Developers Workshop

Core Rot: cfprefsd

More inexplicable expected behaviour. By Joel Bruner.


Buy It

Try It

So, I've been keeping busy, no time for blog posts... However thought you'd find this interesting because while it affects me on the scripting side, it also is at the heart of the APIs that you well know Apple has been constantly fiddling with to the point of uncertainty.

(Maybe I will just post this long rambling thing as a blog post...)

So as of 10.8 writing to a plist with anything but /usr/bin/defaults (at least out here in script-land) is not guaranteed to actually change the plist!

From:

https://developer.apple.com/library/mac/#releasenotes/CoreFoundation/CoreFoundation.html

CFPreferences Plist Files

The on-disk property list files used by CFPreferences have always been a private implementation detail, but in previous releases, directly modifying them has mostly worked (though there are some potential data-loss issues for applications that do so, even on previous systems). In 10.8 and later, the CFPreferences agent process (cfprefsd) will cache information from these files and asynchronously write to them. This means that directly modifying plist files is unlikely to have the expected results (new settings will not necessarily be read, and may even be overwritten). You should use the NSUserDefaults or CFPreferences APIs, or the defaults(1) command, to interact with the preferences system.

[Note: There are several warning lights already there. 'Potential data loss'? 'Mostly worked'? Ed.]

This means PlistBuddy, the only other Apple tool one can use on plists with an even greater degree of control to dig into heavily nested dicts and arrays, is not guaranteed to work anymore!

So the new daemon that is caching these plist values to memory and flushing them out every so often is cfprefsd.

CFPREFSD(8)               BSD System Manager's Manual              CFPREFSD(8)

NAME
     cfprefsd -- defaults server

SYNOPSIS
     cfprefsd

DESCRIPTION
     cfprefsd provides preferences services for the CFPreferences and NSUserDefaults APIs.

     There are no configuration options to cfprefsd manually.

Mac OS X                      October 25th, 2011                      Mac OS X

And it has been an issue for developers too, since last summer:

http://www.cocoabuilder.com/archive/cocoa/317915-cfpreferences-and-mountain-lion.html

I was made aware of this when my loginhooks script that runs though various scripts that do various things at login, one of which is a Dock icon adder, started having the effect of totally obliterating the Dock! Only the last set of icons to be added would survive. The script is addicontodock and it runs as the user logging in, and yes the script uses defaults, yet cfprefsd just murders it. Eliminating the extra logic from addicontodock, I made a script with two lines to test:

/private/etc/hooks/loginhooks/addChess.sh

#!/bin/bash
su $1 -c "defaults write /Users/$1/Library/Preferences/com.apple.dock persistent-apps -array-add '<dict><key>tile-data</key><dict><key>file-data</key><dict><key>_CFURLString</key><string>/Applications/Chess.app</string><key>_CFURLStringType</key><integer>0</integer></dict></dict></dict>'"

# defaults write com.apple.loginwindow LoginHook /private/etc/hooks/loginhooks/addChess.sh

So this runs when you log in. It adds Chess.app to a new temp file, then renames the file to the existing Dock file. It has the effect of killing everything else in the dock:

# fs_usage -w -f filesys | grep -i dock
11:18:47.274882  stat64                            /System/Library/LaunchAgents/com.apple.Dock.plist             0.000005   launchctl.3016008
11:18:47.274899  open              F=5   (R_____)  /System/Library/LaunchAgents/com.apple.Dock.plist             0.000011   launchctl.3016008
11:18:50.615559  lstat64                           /Users/test/Library/Preferences/com.apple.dock.plist          0.000039   ls.3016177
11:18:50.616592  lstat_extended64                  /Users/test/Library/Preferences/com.apple.dock.plist          0.000009   ls.3016177
11:18:50.616599  listxattr                         /Users/test/Library/Preferences/com.apple.dock.plist          0.000004   ls.3016177
11:18:50.619907  lstat64                           /Users/test/Library/Preferences/com.apple.dock.plist          0.000011   ls.3016178
11:18:50.621076  lstat_extended64                  /Users/test/Library/Preferences/com.apple.dock.plist          0.000009   ls.3016178
11:18:50.621082  listxattr                         /Users/test/Library/Preferences/com.apple.dock.plist          0.000004   ls.3016178
11:18:51.030309  open              F=26  (R_____)  /Users/test/Library/Preferences/com.apple.dock.plist          0.000025   cfprefsd.3016087
11:18:51.030410  open              F=4   (R_____)  /Users/test/Library/Preferences/com.apple.dock.plist          0.000018   defaults.3016194
11:18:51.030933  open              F=4   (R_____)  /Users/test/Library/Preferences/com.apple.dock.plist          0.000007   defaults.3016194
11:18:51.040173  open              F=27  (RWC__E)  /Users/test/Library/Preferences/com.apple.dock.plist.Ay8KJ7w  0.009082 W cfprefsd.3016087
11:18:51.124549  rename                            com.apple.dock.plist.Ay8KJ7w                                  0.042862 W cfprefsd.3016087

So I had previously copied in a new default dock and had ls running in a loop in an ssh shell. Here's the before/after:

-rw-------  1 test  staff  4469 Feb 21 11:17 /Users/test/Library/Preferences/com.apple.dock.plist
-rw-------  1 test  staff   348 Feb 21 11:18 /Users/test/Library/Preferences/com.apple.dock.plist
$ defaults read com.apple.dock
{
    "persistent-apps" =     (
                {
            "tile-data" =             {
                "file-data" =                 {
                    "_CFURLString" = "/Applications/Chess.app";
                    "_CFURLStringType" = 0;
                };
            };
        }
    );
}

Yes, that's the entire contents of the Dock following this, it's not much to look at, later a daemon comes by and adds icon data, changes the _CFURLString (prepending file://localhost/) and adds the Downloads folder, but - woopty do! All your icons are gone!

What seems to be happening is that when this script executes as a loginhook, cfprefsd has not even been initialized for that user (an instance is run for every user including nobody, _lp, mdworker, etc) and is not aware of anything being in the Dock plist because it hasn't cached it to memory yet, so it just adds the array to nothing, and flushes it to disk, done - next!

The workaround is not only to have the script sleep 5-15 seconds for cfprefsd gets its bearings, but to also make a proxy script that calls the script with an '&' so that all the other login processes initialize while the add chess script itself is sleeping!

My master loginhook script actually does run the scripts with an '&', so then all I had to do was add the sleep, but GEEZ is that really necessary?! cfprefsd needs to take the hippocratic oath and if unsure of WTF is going on, then double-check and do no harm!

This is the change:

/private/etc/hooks/loginhooks/addChess.sh

#!/bin/bash
su $1 -c "sleep 5; defaults write /Users/$1/Library/Preferences/com.apple.dock persistent-apps -array-add '<dict><key>tile-data</key><dict><key>file-data</key><dict><key>_CFURLString</key><string>/Applications/Chess.app</string><key>_CFURLStringType</key><integer>0</integer></dict></dict></dict>'"

# defaults write com.apple.loginwindow LoginHook /private/etc/hooks/loginhooks/addChessProxy.sh
/private/etc/hooks/loginhooks/addChessProxy.sh
#!/bin/bash
/private/etc/hooks/loginhooks/addChess.sh $1 &

Anyway, I've just been nibbling away at this, first I found the workaround, because what was essential was trying to figure what's going on, to perhaps avoid and maybe even file a bug report, just for the heck of it.

Yet another workaround I just discovered while writing this, which is probably what Apple will tell me to do if they reply to the bug report, is to make the script a launchd user agent, which is then loaded at login, but after all other user processes are initialized it seems and things are not trashed. So just another example of new middle managers being introduced into the ever expanding bureaucracy of OS X.

PlistBuddy started its life bundled inside iTunes installers where it was called by the postflight scripts. 10.5 saw it being stashed at the unsearched path /usr/libexec, but at least it was there (in the OS). And after getting used to its quirky command line, it became my go to plist tweaker.

But I think it's more of a Apple hobby/quick and dirty tool than anything and probably forgotten by anyone hired in the past few years. Who are probably the same authors of cfprefsd. Also note the man page for defaults and the BUGS section which admits of its limitations, there since its first appearance.

Joel Bruner is a Macintosh Systems Administrator for a marketing conglomerate with 250+ Macs in his charge. His blog is found at http://brunerd.com.

About | Buy | Forum | Industry Watch | Learning Curve | Products | Search | Twitter | Xnews
Copyright © Rixstep. All rights reserved.