One feature of Klicko which is approaching buglessness asymptotically is auto-update.
I frequently get asked why I’m not just using Sparkle. There are two main reasons: first, it’s somewhat bulky and generic. Look at the latest version of ClickToFlash: it’s 1.4MB, of which 1.1MB are used by Sparkle. (Why this matters to me may best be explained in another post; the same goes for my generic dislike of in-app frameworks.)
Second, Sparkle does not have automatic updating as such; it normally checks when its application (or plug-in, or whatever) is run. Klicko, as a System Preferences panel, is mostly a set-it-and-forget-it piece of software; my experience was that few users remember to check for updates regularly, or re-open the panel at all. So I need periodic checking in the background process; however, Klicko’s background process is constrained to not use AppKit or show any UI, so it can’t use Sparkle either.
That said, updating applications or preference panels is in many ways a tricky business. One of the problems is that, unlike Classic applications, Mac OS X applications or bundles are composed of a folder hierarchy, and finding items in that hierarchy is usually done by APIs that turn out to be path-based. If you move (or, worse, substitute) a running application or plug-in, it may fail in interesting ways: things like images or executable code may be cached from the old version, and others will be pulled in from the new version.
In other words, it’s not advisable to have a running app delete itself and replace its bundle by a freshly-downloaded one. In the case of a preferences panel, System Preferences (at least in Leopard) doesn’t properly unload and uncache a panel when installing a new one. The solution is to quit the application and have a different process do the swap-in and immediate re-execution of the updated application.
The swap-in part is easily handled by FSReplaceObject() or its cousin, FSPathReplaceObject(); just make sure of passing kFSReplaceObjectDoNotCheckObjectWriteAccess|kFSReplaceObjectPreservePermissionInfo in the options argument, and everything will work. (However, this may require user authorization for replacing bundles in, say, /Library/PreferencePanes.)
For simplicity, let’s show only the re-execution part. Sparkle uses a shell script to do so. Slightly simplified, it looks like this:
setenv ("Executable_PATH", path, 1);
system ("/bin/bash -c '{ for (( i = 0; i < 3000 && $(echo $(/bin/ps -xp $PPID|/usr/bin/wc -l))-1; i++ )); do\n"
/bin/sleep .2;\n"
" done\n"
" if [[ $(/bin/ps -xp $PPID|/usr/bin/wc -l) -ne 2 ]]; then\n"
" /usr/bin/open \"${Executable_PATH}\"\n"
" fi\n"
"} &>/dev/null &'");
Don’t expect me to explain line-for-line what this does; I’m not a shell scripting guru. From what I understand, it runs the ps utility to wait until the parent process quits, then opens the path passed in. This solution has the advantage of not requiring a separate tool to do its work. The disadvantage is that spawns several auxiliary processes; a shell, the ps tool, and so forth; often it also logs some cryptic errors to the system log.
In Klicko, since I already had to write an auxiliary tool for other purposes, like installing and uninstalling, it was easy to just add another function to it. But, for simplicity, let’s assume the tool just does wait for its parent process to quit and opens its argument path. There’s a simple way of doing so, by using a pipe. Pipes are already used for communicating between parent and child processes, anyway. Here’s how you could launch the tool using NSTask:
NSTask* task = [[NSTask alloc] init];
[task setLaunchPath:@"/path/to/tool"];
[task setArguments:[NSArray arrayWithObject:[[NSBundle mainBundle] bundlePath]]];
[task setStandardInput:[NSPipe pipe]];
[task launch];
[NSApp terminate:nil];
and the tool would look like this:
int main(int argc, char **argv) {
char dummy;
read(STDIN_FILENO, &dummy, 1);
CFURLRef url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (UInt8*)argv[1], strlen(argv[1]), FALSE);
LSOpenCFURLRef(url, NULL);
}
Notice that we launch the NSTask with a dummy NSPipe, and terminate immediately afterwards without waiting for the tool to complete. This means that the sending end of the pipe is shut down when the application terminates. On the tool side, we have a read() call trying to get a single byte from the pipe. The parent application never sends anything, so it hangs there until the application terminates and the pipe is shut down; the read statement will return an error (which is ignored) and the tool then calls LaunchServices to re-launch the application.
If you already have all the paths in C string format, and don’t want to use Cocoa for calling the tool, here’s an alternative solution on the application side:
int fildes[2] = {0,0};
if (!pipe(fildes)) {
if (!vfork()) {
close(fildes[1]);
if (dup2(fildes[0],STDIN_FILENO)!=STDIN_FILENO) {
close(fildes[0]);
}
err = execl(pathToTool,pathToTool,pathToApplication,NULL);
_exit(EXIT_FAILURE);
}
close(fildes[0]);
}
[NSApp terminate:nil];
which does essentially the same thing using BSD APIs.
Many thanks to Mike Ash for explaining the pipe trick to me, and correcting my misunderstanding about child process lifetimes; I originally thought that a child process wouldn’t survive the termination of its parent. Apparently this is true only for shell processes.
Leave a Comment