Solipsism Gradient

Rainer Brockerhoff’s blog

Browsing Posts in Software

Hold on

No comments

Well, since I last wrote, a reasonably definite version of Klicko has been published (1.1.1 build 207) and I deemed the supporting code to be mature enough to serve as basis for the next version of Quay. So, since then, I’ve been busy on that.

On June 4th I’ll be arriving in San Francisco – WWDC starts on June 8 – and hopefully by then I’ll have an early alpha version of Quay 1.2 (or maybe 2? II?), and of the Quay Plugin Developer Kit as well. Stay tuned.

Posted by 0xced:

Rainer Brockerhoff wrote:

   [task launch]; 
   [NSApp terminate:nil];

No need to pass the calling program’s process identifier, and it works from Tiger (10.4) on up.

Although unlikely, the task may be faster to execute than [NSApp terminate:] and the relaunch process would thus fail. By listening to the termination of the calling process, you are guaranteed to always relaunch.
Edit: I’m wrong, Rainer just pointed to me that the task can’t terminate since its first line is a read on the input pipe and it will block there.

Cédric Luthi has posted another take on the mechanics of reopening a System Preferences pane (or is it panel?). Worth a read; however, I would propose a shorter piece of code for his relaunch snippet, combining two of my recent posts:

int main(int argc, char **argv) { 
   char dummy; 
   read(STDIN_FILENO, &dummy, 1); 
   CFURLRef url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (UInt8*)argv[1], strlen(argv[1]), FALSE); 
   CFArrayRef aurl = CFArrayCreate(kCFAllocatorDefault, (const void**)&url, 1, NULL);
   FSRef ref;
   if (LSFindApplicationForInfo(0, CFSTR("com.apple.systempreferences"), NULL, &ref, NULL)==noErr) {
      LSApplicationParameters parms = {0,kLSLaunchDefaults,&ref,NULL,NULL,NULL,NULL};
      LSOpenURLsWithRole(aurl, kLSRolesAll, NULL, &parms, NULL, 0);
      }
   }
}

and the code to launch this tool (from within the preference panel itself) is:

   NSTask* task = [[NSTask alloc] init]; 
   [task setLaunchPath:@"/path/to/tool"]; 
   [task setArguments:[NSArray arrayWithObject:[[NSBundle bundleForClass:[self class]] bundlePath]]]; 
   [task setStandardInput:[NSPipe pipe]]; 
   [task launch]; 
   [NSApp terminate:nil];

No need to pass the calling program’s process identifier, and it works from Tiger (10.4) on up.

Also, yikes. 26 days without posting! I plead temporary insanity brought on by tax filing time, and Twitter – the latter being a more convenient outlet for short links and thoughts.

Anyway, taxes are filed and we’re now having a short working vacation in the hills of Petrópolis, an old town north of Rio de Janeiro. Dorinha is taking a short English immersion course (excellent BTW), and I’m coding again, yay!

I’m patching up some loose ends in Klicko in preparation to cloning its preference panel for the next version of Quay, as I’ve mentioned before. While doing that, I’m also trying to refactor my code into a tighter and more readable form. Some of that might be interesting…

For instance, the automatic update checker has a dialog button to “Open System Preferences” and this should go to the Klicko preference panel. Now, System Preferences may already be running but with another panel selected; in any event, the Klicko panel should be opened and ready for the user to see update details. There are several ways to accomplish this.

Most people probably will consider, at first, writing an AppleScript to open System Preferences and then select the Klicko preferences panel. This is unnecessarily complex, and I’ve looked at several solutions. The simplest one-liner to do so from Cocoa would be:

[[NSWorkspace sharedWorkspace] openFile:@"/full/path/to/my.prefPane"];

There’s a non-obvious down-side to that: NSPreferencePane is generic and may be implemented by other apps for their preference plug-ins. Someone’s application might use it and declare .prefPane in its Info.plist. This would in my opinion be a mistake, in that double-clicking or running the code above might (or not) open that other app instead of System Preferences!

The solution I finally hit upon uses Launch Services to open the correct application with the preference panel, like this:

FSRef ref;
if (LSFindApplicationForInfo(0, CFSTR("com.apple.systempreferences"), NULL, &ref, NULL)==noErr) {
   LSApplicationParameters parms = {0,kLSLaunchDefaults,&ref,NULL,NULL,NULL,NULL};
   NSArray* args = [NSArray arrayWithObject:[NSURL fileURLWithPath:@"/full/path/to/my.prefPane"]];
   if (LSOpenURLsWithRole((CFArrayRef)args, kLSRolesAll, NULL, &parms, NULL, 0)==noErr) {
      // success!
   }
}

This code first finds the System Preferences app by its bundle ID, and makes a FSRef for it. The FSRef is then pointed to from the LSApplicationParameters structure, and passed to LSOpenURLsWithRole; this will run System Preferences if it’s not already running, and tell it to open the panel.

It’s tempting to pass the panel’s path as an argument inside of LSApplicationParameters. This does indeed work if System Preferences is not already running, but unfortunately it’s ignored if it is.

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.

Klicko 1.1 (201) is up, with the aforementioned full keyboard access/VoiceOver support…

An hours-long outage somewhere inside my ISP made me re-evaluate the idea about a reachability transition callback. It seems to be reliable enough when the local network is involved, but when the clogged pipe is several routers away, it can go down – and never go up again.

So what I’m doing now is, while the network is down, retry with a direct reachability test every half hour or so; of course if the callback says it’s back up, I tear that timer down again. Seems to work OK.

Several interim builds of Klicko went up in the meantime, implementing this and other small changes. The latest one is now in testing; build 1.1 (201) will implement full keyboard accessibility and VoiceOver support. At least in the preferences panel part; the background process is still mouse-oriented, and I don’t foresee disabled people needing any of the Klicko functions. Still, it’s good practice since nearly all of the Klicko panel code will be re-used in the upcoming rewrite of Quay, and there it will certainly be useful.

Re: Klicko 1.1

No comments

And, the reachability check explained below is now also present in Klicko 1.1 (189).

Photos licensed by Creative Commons license. Unless otherwise noted, content © 2002-2024 by Rainer Brockerhoff. Iravan child theme by Rainer Brockerhoff, based on Arjuna-X, a WordPress Theme by SRS Solutions. jQuery UI based on Aristo.