Saturday, August 7, 2010

Apple refuses to solve my installer problem

A while ago I wrote about two problems I was having with the 10.6 installer and then a little while later how I had solved one of them with a bit of a hack. I had filed the other one as a issue with Apple, and I got a response back on the bug recently.

As a quick refresher: The problem is that there are a number of installers, both from Apple and from third parties, contain scripts that make the assumption that you are always installing on the root volume. Obviously this is a problem with things like InstaDMG, DeployStudio, or even System Image Utility. I managed to solve this class of problem for 10.5 by wrapping the installer in a chroot jail, a solution that worked better than I had hoped. Unfortunately the 10.6 installer breaks when I try to wrap it the same way. My best guess is that it is dying while trying to enumerate the volumes so that VolumeCheck scripts can run... but when using the command line version they are never run.

The answer I got back from Apple on this was a single line of text telling me that installers need to be written correctly to target non-boot volumes. I am more than a bit disappointed and angry at this response from Apple, as it means that this problem will not be fixed, and those in charge of fixing it do not see it as a problem and see the answers to this situation as lying with others. There are a few problems with this attitude:

  1. Apple has proven on more than a few instances that it is not capable of consistently authoring packages that do the right thing in these cases. iTunes, the iLife Updaters, and iWork installers are just a few cases. If Apple can not get this right then what hope is there that third parties will get it right (even accepting that Adobe will never get within visual distance of getting it right).
  2. Apple does have a product that needs exactly this setup: System Image Utility. Both in the NetRestore-from-installer and the NetInstall paths the installer needs to work on non-booted volumes with a variety of packages. That the SIU team has been very slow to acknowledge problems with their approach in this area is frustrating. They got the iTunes installer finally working (Ya!), but it took me yelling at them personally at a conference for it to happen (Booo!). I expect more from Apple.

So what do I do now? How can I overcome this class of issue? I have a few possibilities:

  1. Come up with some brilliant solution that tricks the 10.6 installer into working inside a chroot jail
  2. Create a system that unwrapped every form of .pkg (and there are a number of formats), replaces all of the scripts with a version that is wrapped with a chroot jail
  3. Write my own version of the installer that does things right
  4. Yell at Apple for a while, and get other people to do so as well in the hope that this decision will be reversed
  5. Convince every .pkg author out there to write their installers to work on non-booting images
  6. Just accept that some installers will never work in InstaDMG, and hope that one of them is never in a software update, or something else absolutely required (ie: give up)

The first three items are ones that I could in theory do (although there is probably no hope for the first, and the latter two are going to be difficult). Of the last three the fourth item is the one that I wish would work (it would be the best solution), and the last one is the one I am most afraid of.

So, if anyone is reading, and would like to do something, please tell Apple how much value there is to you in installers working on non-boot volumes. If you would like to mention the Radar number 7699285 that would be great.

Sunday, June 20, 2010

Bug with hdiutil and symlinks

I got an error report from an InstaDMG user who was using symlinks to point at their installer DVD. I had never tried out using symlinks for that, and so tried it out, successfully, and wrote back saying that it was working for me (with a much newer version of InstaDMG), and that they should probably be using the -I flag to specify the disk rather than use a symlink. But I did do some more testing while I was setup this way, and ran into a problem just once after a couple of runs of InstaDMG.

It turns out that there is a bug in hdiutil, at least on 10.6.4, when it comes to resolving symlinks. But this bug only seems to come out on some percentage of runs, and even then the percentage seems to vary with the hardware (or some other variable). On my iMac8,1 I see it 15-25% of the time, while with my iMac5,1 I only see it 0.5% of the time. Granted the older iMac is running a brand-new install, where the newer iMac is running an OS that I constantly beat on.

I have reported this back to Apple as Radar number 8111753, as well as on OpenRadar. But I am curious if other people are getting error numbers like I am, so if you would like to run the following script a few times on your system and post the results in the comments that would be great.

#!/bin/bash

# print the system information
/usr/sbin/system_profiler SPHardwareDataType SPSoftwareDataType | /usr/bin/awk '/Model Identifier:|System Version:/ { $1 = ""; $2 = ""; gsub(/^[ \t]+|[ \t]+$/,""); print }'

# create a temproary folder with three items in it
TEMP_FOLDER=`/usr/bin/mktemp -d /tmp/hdiutilBugTest.XXXX`
/usr/bin/touch "$TEMP_FOLDER/a"
/usr/bin/touch "$TEMP_FOLDER/b"
/usr/bin/touch "$TEMP_FOLDER/c"

# create a compressed image from the temp folder
/usr/bin/hdiutil create -srcfolder "$TEMP_FOLDER" "$TEMP_FOLDER/testImage.dmg" 1>/dev/null

# create the symlink to the image
/bin/ln -s "testImage.dmg" "$TEMP_FOLDER/symlink"

SYMLINK_PATH="$TEMP_FOLDER/symlink"
ABSOLUTE_PATH="$TEMP_FOLDER/testImage.dmg"

PATHS[0]="$SYMLINK_PATH"
PATHS[1]="$ABSOLUTE_PATH"

REPEAT_COUNT=1000
IFS=$'\n'
for THIS_PATH in ${PATHS[@]}; do
 echo "Working on: $THIS_PATH"
 FAILED_COUNT=0
 i=0
 while [ $i -lt $REPEAT_COUNT ]; do
  /usr/bin/hdiutil imageinfo "$THIS_PATH" 1>/dev/null 2>/dev/null
  if [ $? -ne 0 ]; then
   let FAILED_COUNT=FAILED_COUNT+1
  fi
  let i=i+1
 done
 echo "  Failed $FAILED_COUNT out of $REPEAT_COUNT times"
done

# delete the temp folder
if [ ! -z "$TEMP_FOLDER" ] && [ -d "$TEMP_FOLDER" ]; then
 /bin/rm -rf "$TEMP_FOLDER"
fi

Tuesday, June 8, 2010

html timer

For my presentation at Macworld in January I created a semi-time-lapse screen capture of a complete InstaDMG run to run as a demo. Since different parts of it were going to fly by at different rates I wanted to have some sort of timer to show the real clock time. Looking around for some little application or widget I did not find anything I like, and I finally gave in and made one myself.
Since I wanted this done fast, and with something I could easily control with AppleScript (sense the rest of the demo was being driven by it anyways), I decided to create a little JavaScript timer, and run it inside Safari.
<html>
<head>
    <title>Timer</title>
    <script>
        var hours = null, minutes = null, seconds = null
        var startTime = null
        var currentTimer = null
        
        function startTimer() {
            // setup things
            hours = document.getElementById("hours")
            minutes = document.getElementById("minutes")
            seconds = document.getElementById("seconds")
            
            startTime = new Date()
            displayTimer();
        }
        
        function displayTimer() {
            
            currentTime = new Date(new Date() - startTime)
            seconds.innerHTML = currentTime.getUTCSeconds()
            minutes.innerHTML = currentTime.getUTCMinutes()
            hours.innerHTML = currentTime.getUTCHours()
            currentTimer = setTimeout('displayTimer()',500);
        }
        
        function stopTimer() {
            clearTimeout(currentTimer)
        }
        
    </script>
    <style>
        body
{ font-size: large }
        div
{ width: .65in; display: inline-table; font-size: .5in; text-align: right }
    </style>

</head>
<body>
    <div id="hours">0</div> hrs <div id="minutes">0</div> min <div id="seconds">0</div> sec
</body>
</html>
Then I just had to trigger it with some code like:
tell application "Safari" to do JavaScript "startTimer()" in timerDocument
Edit: figured out the problem with the hours, and the correction was to use UTC time.

Saturday, May 1, 2010

Using plists from Python

Python is my current scripting-language-of-choice for a number of reasons, but one of them is that I can handle plists easily, including complex ones, without having to worry about the format that they are in (xml, binary, or even old-style NeXT). I should put the caveat up-front here that this will only work in 10.5 and later, but at this point I don't touch 10.4 machines, and don't anticipate ever working with 10.3 again. So if you can work with that then this method might be for you.
I use the Cocoa bridge to get access to MacOS X's native Foundation layer and the native plist processing available to Obj-C programmers. I know a few other scripter/programmers who use similar techniques in their work, but so far everyone else has been using NSDictionary's dictionaryWithContentsOfFile method. This is great and works well for most plists that you will use, but there are two things you lose by using it:
  1. It will read in all of the native plist formats, but the you don't know what format you started with. I like writing things back down in the format I found them in. It probably does not ever matter, but what can I say? In my job I am a little anal about things like this.
  2. Not all plists have a dict as their root, some have NSArrays. You probably know going in what the format of the plist you are working with should be, so this is not such a big deal, but I like to be able to be a little more specific about what went wrong why my programs bail.
The solution for these two issues is to use the NSPropertyListSerialization class to read from, and write out your plists. This is easy to do, and the best explanation of it is to give an example, first a minimal one:
#!/usr/bin/python pathToPlist = [insert path here] plistNSData, errorMessage = Foundation.NSData.dataWithContentsOfFile_options_error_(pathToPlist, Foundation.NSUncachedRead, None) plistContents, plistFormat, errorMessage = Foundation.NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(plistNSData, Foundation.NSPropertyListMutableContainers, None, None) # plistContents is now a tree with the data plistNSData, errorMessage = Foundation.NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_(plistContents, plistFormat, None) suceeeded, errorMessage = plistNSData.writeToFile_options_error_(pathToPlist, Foundation.NSUncachedRead, None)
Important note: Blogger is probably cutting off the ends of lines on the display, and wrapping others. But a copy-and-paste should get you what you need. You also have to fill in the path to your plist of choice there, and this does not do anything other than read the plist, and write it back down unchanged. But if you are looking for a quick cut-and-paste that is probably what you want.
For a more complicated example lets make sure that Acrobat has not been set as the default handler for PDFs. This pulls out most of the stops and checks for all types of problems, so should be a much better example to follow for production code:
#!/usr/bin/python '''This script sets the default file opener for PDFs to Preview''' import os, sys, Foundation # get the path to this user's LaunchServices preference file pathToLaunchServicesPlist = os.path.expanduser("~/Library/Preferences/com.apple.LaunchServices.plist") if not os.path.isfile(pathToLaunchServicesPlist): raise Exception("The LaunchServices preferences file seems missing: %s" % pathToLaunchServicesPlist) # read out the data in the file plistNSData, errorMessage = Foundation.NSData.dataWithContentsOfFile_options_error_(pathToLaunchServicesPlist, Foundation.NSUncachedRead, None) if errorMessage is not None or plistNSData is None: raise Exception("Unable to read in the data from the plist file: %s\nRecived error message: %s" % (pathToFinderPlist, errorMessage)) # convert the data into a useable form launchServicesPreferences, plistFormat, errorMessage = Foundation.NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(plistNSData, Foundation.NSPropertyListMutableContainers, None, None) if errorMessage is not None or pathToLaunchServicesPlist is None: raise Exception("Unable to read the data as a plist: %s\nRecived error message: %s" % (pathToLaunchServicesPlist, errorMessage)) # launchServicesPreferences is now a tree of objects that we can modify with normal python methods #   but we have to check to make sure it looks like we expect # check to make sure that the root is a dict like we expect it to be # Note that the root is actually a NSDictionary object, # but this is bridged to work everywhere at python dict object would. # But it is not actually a dict object if not hasattr(launchServicesPreferences, "has_key"): raise Exception("The plist does not have a dictionary as its root as expected: %s" % pathToLaunchServicesPlist) # confirm the LSHandlers item at the first level, and that it reacts like a python list (really a bridged NSArray) if not "LSHandlers" in launchServicesPreferences or not hasattr(launchServicesPreferences["LSHandlers"], "append"): raise Exception("The plist is missing the LSHandlers section, or it was not an array: %s" % pathToLaunchServicesPlist) # iterate over the array to find any that set the handler for pdfs for handlerSetting in launchServicesPreferences["LSHandlers"]: if hasattr(handlerSetting, "has_key") and "LSHandlerContentType" in handlerSetting and handlerSetting["LSHandlerContentType"] == "com.adobe.pdf": handlerSetting["LSHandlerRoleAll"] = "com.apple.preview" # the setting (if it was set) should now be changed in our in-memory version, we only need to save this back to disk # convert the tree back to a NSData using the same format we read it in with plistNSData, errorMessage = Foundation.NSPropertyListSerialization.dataFromPropertyList_format_errorDescription_(launchServicesPreferences, plistFormat, None) if errorMessage is not None or plistNSData is None: raise Exception("Unable to sealize preferences data. Got error message: %s\nTrying to seraliza data:\n%s" % (errorMessage, launchServicesPreferences)) # write the data back down to disk suceeeded, errorMessage = plistNSData.writeToFile_options_error_(pathToLaunchServicesPlist, Foundation.NSUncachedRead, None) if errorMessage is not None and suceeeded == True: raise Exception("Unable to write preferences back to disk to: %s\nRecieved error message: %s" % (pathToLaunchServicesPlist, errorMessage)) sys.exit(0)

Wednesday, March 17, 2010

Troubleshooting an odd symlink bug

About a week ago an odd bug that was brought to my attention that occurs when people tried to install the Puppet package into an image made with InstaDMG. The bug started out in private emails, but we got it moved over to the developer mailing list, and you can take a look at it. A group of us banged our collective heads over it for a while, and finally I found it by just going over every step to see what was wrong. The problem manifested itself as the Puppet installer overwriting the softlink that you normally find at '/usr/lib/ruby/site_ruby', and instead putting a folder with the desired contents there. Replacing this symlink apparently broke other things, and thus began the bug-hunt. The bug was reported against InstaDMG because the installer works fine when used on a booted volume. My bet is that a similar problem would have manifested if someone had tried installing this to another volume other than the boot volume, thus clearing InstaDMG in this bug, but we didn't think of that at the time. My first instinct was that there was something wrong with the code in the 'installer' program when faced with the complex series of softlinks that it had to follow (a listing of that appears in a moment). I even created a script that mounted a dmg and tried to re-create the problem in a much simpler manner, but with no success. I did repeat the observed behavior, and knew that there was a problem in there somewhere, so I decided to try and figure out what was different about the softlink chain in this case from my test case. So I carefully followed the chain of symlinks on a mounted volume (InstaDMG output dmg, since I have a few of those lying around). Here is what I found: /usr/lib/ruby -> ../../System/Library/Frameworks/Ruby.framework/Versions/Current/usr/lib/ruby /System/Library/Frameworks/Ruby.framework/Versions/Current -> 1.8 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/site_ruby -> ../../../../../../../../../../Library/Ruby/Site So if you follow this set of rules, on a booted volume '/usr/lib/ruby/site_ruby' winds up pointing at '/Library/Ruby/Site'. My understanding is that this the same in both 10.5.x and 10.6.x, but was different in 10.4. But if you are careful you would count the number of back-references in that last link. There are 10. But if you count the number of folders in the chain back form the 'site_ruby' folder you will only find 9. When you are booted this does not matter, once you are at the root directory you can just keep back-referencing all you like, and you still wind up in the same place. As a quick demonstartion you can do this in the Terminal: 'cd /; cd ..; pwd' and you will still be at root. But when the volume is mounted this means that the 'site_ruby' link winds up pointing back outside the image. So this explains the bad behavior: when the installer goes to look for the folder at this point it finds a broken symlink, so instead replaces that broken symlink with a valid folder. A pretty reasonable thing for the installer to do. I might have made this a failing error if I were the one programming it, but I am sure a lot of smart people came together in a meeting at Apple (or possibly NeXT) at some point in the past and decided that this was the correct behavior, and I can't call them wrong. When I started looking back, it seems that this extra back-reference has been in place since 10.5.0, and has been kept all the way through 10.6.2 (and it might continue). It has been masked all along because it only becomes a problem when you are not booted from the volume. I really should write up a small tool to comb through the whole filesystem and see if there are any other similar problems in any other symlinks, and report those as well in another Radar report, but I think I will leave that for another day. But I thought I would get this out there so if anyone else runs into some other similar bug they might remember this.

Wednesday, March 3, 2010

One installer issue down, one to go

I wrote recently about the two issues I have been trying to solve with some bad installers in 10.6. Well with rev261 of InstaDMG I now have one of the issues solved. The solution is exactly as I described: replace the launchdaemon offering the installd service with one that is chrooted into my install target. I rain a pair of tests with the new version of InstaDMG on a 10.6.2 vanilla image: one with the new code, and one with it disabled (there is a switch for that). The results were exactly as I had hoped for: the iLife Support Update 9.0.3 components get installed with the new code, but get left out of the one without the new code.
So I am marking one of those two issues worked-arround. I wish that the solution to the other one suddenly presents itself, but I am not going to hold my breath, as I am pretty convinced that that one is going to take Apple making a change to solve.
As always, if you want it solved, then tell Apple how this is affecting you, and how many purchases it is affecting. This is not strictly a bug with Apple's code (at least not in the installer binary), so they are likely to not see it worth the engineer time to make the changes unless we can show them reason that it is worth the time (that would otherwise go into other features/changes/fixes).

Tuesday, March 2, 2010

JavaScript reference for Apple .pkg makers

I have never seen it mentioned anywhere and just stumbled across Apple's developer documentation on the AppleScript objects that are available to installer writers. I probably have totally missed something obvious telling me where to find it, but I have always just used what I have gleaned from taking Apple's installers apart, but now have actual documentation:
For those writing scripts that check for the presence of things to decide what to install (I am looking at you iLife Support team) I will pointedly reference the "target" item, and it's "mountpoint" property.