October 07, 2005
Challenge-Response:Safari/Preview Whine Whine

I was hanging out on the internets complaining about things as I always do. This time I was complaining about how the Safari downloads window doesn't track files if you move or rename them. I thought this was unacceptable since the Mac OS has always had an excellent method of tracking files. The problem is that these Unix/Cocoa programmers that are new to the Mac OS don't know about them or have no way of using them in their "preferred" API set. And they won't dare use an API that isn't their preferred one even if it gets the job done, does it a lot better, and presents the user with a much better experience. Anywho, I took these complaints somewhere and was basically told, by an engineer that will remain nameless, that I should just write a haxie to fix it. So I did. Since the Safari problem and the extremely lamely responded to Preview bookmark bug are the same core problem, I decided to tackle them both at the same time.

Both of these problems are similar to iSWAD in that they address a bug in Mac OS X before Apple could. In this case, I don't care if Apple obsoletes this piece of third party software. In fact, I yearn for it.

Safari

Update:The Safari portion of the problem is fixed in Safari 2.0.2 which is part of Mac OS X 10.4.3. It adds a new instance variable called _alias of type AliasHandle to the DownloadFile class. Of course, -[DownloadFile path] is still called but if it fails it calls a new method, -[DownloadFile aliasPath] and which resolves the alias into a path and works on it like that. This is actually pretty much identical to the method I used in SPWW (pronounced like "spew") except I never bothered to see if the path was valid or not. This new alias data is store in the DownloadEntryAliasBlob and DownloadEntryPostAliasBlob keys in the downloads file (~/Library/Safari/Downloads.plist). And much like SPWW, Safari 2.0.2 does not generate aliases for old downloads especially if they don't exist on the hard drive any more.

There is no harm in keeping SPWW installed even if you have Safari 2.0.2 installed. They do not conflict with eachother.

The Safari problem was a lot more daunting than it seemed to be at first. The problem with Safari is that it stores paths for everything, everywhere. The DownloadProgressEntry class has an instance variable of a DownloadFile instance. This instance then contains some paths. And there's a dictionary somewhere that contains the paths to files that Safari constantly queries to get a path. What I ended up doing here was patch the getting of the DownloadEntry*Path keys to look for an alias in the same dictionary prefixed with SPWW. I create these keys when Safari goes to save new downloads to its preferences file (~/Library/Safari/Downloads.plist). So then when Safari loads its downloads history, it gets the aliases along with it, and when it asks for the path keys, I redirect it to the alias keys and resolve the aliases then to get a path that it is expecting. Really simple.

The hard part was attaching an alias to DownloadFile instances. I can't add an instance variable to the class. So I ended up creating a new global variable that used the pointer values of the instances of the DownloadFile class as keys. Using the pointer value instead of the instance itself was done so that there would be no recursive retains. So I could patch the -[DownloadFile dealloc] method to release the memory used by the alias that was being used to track the download file. I also had to patch -[DownloadFile setPath:] so I could replace the alias with one with the new path.

This was all working quite nicely and I begun to test it out. It backfired. My alias resolving was working too well. When you download a zip file or some disk images in Safari, Safari will automatically decompress them and throw the original in the trash or mount the image, copy the application of the disk image, and throw the disk image in the trash. Safari checks to see if the trashing was successful by seeing if the file exists at the path and expects it to not exist. The problem was that my alias tracking was updating the path to the file's new location in the trash. So I'd see errors that said "Cannot Move File" in Safari's download window. Argh. Luckily DownloadFile has a method that tells whether or not the file was moved to the trash. If this returns true, I delete the alias tracking information from the global dictionary. This fixed the problem and all was well with the world.

This fix only works for new downloads in Safari, not existing ones. For Apple to fix this bug, they'd have to add an FSref instance variable to DownloadFile and resolve that whenever the path is requested. And then whenever Safari saves these to disk, convert them to an AliasHandle. Then when loading, convert the AliasHandle back into an FSRef and initialize the DownloadFile with that. Make sure to check -isTrashed first!

Preview

Preview's a much funnier case (funny like a clown being blown up in front of small children). It doesn't use aliases to track bookmarks or the last page you had open in a document. So if you move a bookmarked PDF, you lose the ability to use the bookmark. As you can see from the previous link, this was reported to Apple. Some engineer said it wasn't possible. Everyone laughed in the previously described manner. And I got slightly ticked off.

The most amusing part about this is that NSDocument (the thing that handles basic document operations in Cocoa applications) actually uses FSRefs to track the document. So you can move and rename an open document in the Finder then go back to Preview and see that the window title for that document has properly updated itself. But all bookmarks for that document are now broken. Sigh. The other sad thing about NSDocument is that although it uses FSRefs to track files, it does it in a horrible way. Rather than resolve the FSRef whenever someone asks for a path, it updates the path whenever its owning window is brought forward. This is just pointless, really. There is no reason whatsoever to update it then compared to converting it to a path on a "just in time" basis. FSRefs don't have the larger overhead of Aliases either and, like aliases, they can still track files if moved or renamed. The only problems with FSRefs are that they cannot refer to files that don't exist and you cannot save them to disk and expect them to work when loading them back into memory. They are not persistent while aliases are.

What SPWW ends up doing for Preview is to patch out -[NSDictionary objectForKey:] with the same idea mentioned above as Safari. It checks another key in the dictionary and resolves the alias then, on the fly. The problem becomes the bookmark menu items, which are just... dumb. I have no other way to describe what is happening. It sets a represented object on the NSMenuItem. This object is just the path to the file. When it validates the menu item, it checks to see if this path is valid. Understand that this path is not attached to the dictionary at all. It is completely independent. SPWW sets an AliasHandle on the menu item instead and resolves that into a path when it is asked for (Preview still gets a path, it's just the correct one).

The tricky part about Preview is how it saves bookmarks. SPWW was correctly adding aliases to the bookmark, but Preview wasn't seeing that because it wasn't reloading its information to be in sync with the SPWW modified dictionary. So now SPWW just resets Previews' preference dictionary when a bookmark is saved. This works very well.

In order for Apple to fix this one, they'd have to create a new instance variable for PVPDFBookmarksController. It'd be an array that contained instances of a brand new bookmark class. This bookmark class would contain an FSRef and would be resolved and initialized the same was as the DownloadFile class should be in Safari.

End Thing

In a perfect world paths would never be used in software. They're evil.

Just like iSWAD, the source for SPWW is available. It also demonstrates some concepts used when writing APEs like context patching. This is when you patch something that isn't called a lot and patch something inside of that patch that is called way too often and finally remove the patch after the original returns. It also uses the method of providing a context (in this case a controller instance) so the inner most patched function has some idea of what called it so it can call functions that affect the controller. SPWW also demonstrates modifying the Info.plist of the APE Module so it only loads in two applications. It won't even load outside of Preview and Safari.

The linked disk image contains the source code and the compiled APE module. You'll need to download Application Enhancer to use the APE module (install it in ~/Library/Application Enhancers/, create the folder if it doesn't exist) and if you want to compile the project you'll need Xcode 2.1 or later and the APE SDK. Remember, you must log out if you are installing APE for the first time.

Download Safari/Preview Whine Whine.dmg (65k)

Digg This!

 Posted by rosyna at October 07, 2005 02:26 PM

Trackback Pings:

TrackBack URL for this entry:
http://www.unsanity.org/mt-tb.cgi/343.




Related:
Comments

Pwned!

Posted by: Chris Biagini on October 7, 2005 3:00 PM

Related, but not SPWW. In Mac OS 7 and 8, if a file arrived as an email attachment, the email address of the sender was in the comment box if you "Got Info". That was useful. Is it possible after you're happy with with SPWW?

Posted by: Geoff on October 8, 2005 11:18 PM

Wow - nice work.

My personal wish for the Safari downloads window would be that the file icons were true icon proxies - so you could drag them out of the window and onto applications. They're just images currently.

Posted by: Diggory Laycock on October 10, 2005 3:41 AM

if i were a man that bets, i'd say aliases as self-readjusting references are not long for this world. the concept does not exist in most non-HFS filesystems which the Mac deals with more and more, so that behavior quickly becomes the quirk in the user experience.

curiously, i've been using a Mac since the beginning and have never exploited the fact that aliases survived file moves. i wonder how many End User people would notice (present company excepted, of course) if that part stopped working?

the problem is that the way aliases were originally implemented in MacOS exposed deep information about the implementation of the filesystem. a more generic interface would require a filesystem to export a stable designator for a file which was independent of the naming machinery and then provide inverse lookups. some filesystems do not have such a designator - the file metadata is stored in the directory entry. move the file, the metadata moves with it. no (other) filesystem i know of supports doing the inverse lookups efficiently. that efficiency is an artifact of the particular implementation of the HFS naming machinery. what would be really interesting to know is how much support for "Alias Classic" behavior figured in the design of the HFS naming machinery.

note that i'm not arguing that "Alias Classic" behavior is not useful - i can understand why it would be to some people. but the semantics are highly localized and don't scale beyond local filesystems, if that. therefore, that behavior has negative survival value, long term.

however, to argue the other side for a moment, with the popularization of "naming-independent file navigation" (eg, MacOS X Spotlight and MS Longhair) there is an underlying notion of files having existence that transcends the filesystem naming machinery. how this impacts the objects and interfaces exported by filesystems remains to be seen, although it will likely have some impact. whether it has enough to allow a generalized notion of "Alias Classic" behavior to be implemented across filesystems will be interesting to watch. i suspect what will happen, however, is that "Spotlight" will be pushed as the more general lookup method, so the inverse mappings might be done through that. Assuming that has acceptable performance and stores enough metadata to provide the inverse map, life might be good after all for "Alias Classic".

this will be interesting to watch unfold.

thanks for raising a very interesting question.

Posted by: Mike O'Dell on October 13, 2005 6:56 AM

Just because having a worse experience for the user is the norm, doesn't mean that OS X should do it. Making the least common denominator the bar that everyone adheres to only ends up hurting users. OS X has a much better implementation and that's what should be used on OS X.

Also, aliases/FSRefs/FSSpecs work on all filesystems OS X supports. If the FS supports some kind of unique identifier (like inode), OS X will map that to the volfs system dynamically. See http://developer.apple.com/qa/qa2001/qa1113.html for a very old list. I believe FAT and NTFS are now supported by volfs in 10.4 as well. And even if the volume doesn't support volfs and there is no way to emulate the behaviour, the worse that happens is that aliases are no more persistent than paths.

So aliases will never fail where paths succeed and aliases provide a much better user experience. Remember, you own the files you created, OS X does not. You should have the freedom do move and renamed them as you please. Aliases give you that freedom.

Posted by: Rosyna on October 13, 2005 7:17 AM

I get an corrupt disk image when I download it with Safari.

Posted by: emd on October 13, 2005 7:03 PM

emd, you must be on 10.4 to download it. It's a 10.4 only disk image on purpose.

Posted by: Rosyna on November 1, 2005 8:05 PM
Post a comment
Keep comments on topic. If a comment is unrelated to this post, it may be removed or moderated.





Remember Me?

(you may use HTML tags for style)