Solipsism Gradient

Rainer Brockerhoff’s blog

Browsing Posts tagged Cocoa

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.

Re: Cocoa musings pt.4

No comments

M. Uli Kusterer emailed me to say I’d forgotten that some people still have non-broadband connections. I had supposed that NSURLConnection would simply return an immediate error if the internet link is down – it turns out that it may autoconnect if the user has a dialup connection, for instance.

The way around that is to use the network reachability APIs. Here’s one way to insert that into the example below:

#include <SystemConfiguration/SCNetworkReachability.h>
...
- (id)initWithURL:(NSURL*)theURL delegate:(id)theDelegate {
   if ((self = [super init])) {
      SCNetworkConnectionFlags flags = 0;
      if (SCNetworkCheckReachabilityByName([[theURL host] UTF8String], &flags)) {
         const SCNetworkConnectionFlags mask =
            kSCNetworkFlagsReachable|
            kSCNetworkFlagsConnectionRequired|
            kSCNetworkFlagsConnectionAutomatic|
            kSCNetworkFlagsInterventionRequired;
         if ((flags&mask)==kSCNetworkFlagsReachable) {
            delegate = theDelegate;
            data = nil;
            conn = [[NSURLConnection alloc]
               initWithRequest:[NSURLRequest requestWithURL:theURL]
               cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
               timeoutInterval:60.0]
               delegate:self];
            return self;
         }
      }
      [self autorelease];
      self = nil;
   }
   return self;
} 

This changes the alloc/init pair to return nil whenever the URL host is unreachable… you’ll have to test for that of course.

Update: the downside to the above approach is that the SCNetworkCheckReachabilityByName() function will block until it gets the information – which may take several seconds, perhaps more on some systems. The solution I’m actually using is more complex, installing a reachability transition callback, which sets a global flag that is tested before activating the NSURLConnection.

Cocoa musings pt.4

No comments

One thing I had to do very recently was to download a relatively short block of data from my server. There are some nice Cocoa shortcuts for such a thing, like:

NSError* error = nil;
NSData* data = [NSData dataWithContentsOfURL:theURL options: NSUncachedRead error:&error];

which accesses that URL from the server, presents it to you as NSData and/or gives you an error object. The problem, however, is that it’s synchronous; if your server is down, it may take some time to time-out, and all that time your UI is inactive.
The initial impulse to get around this problem would be to simply spin those two lines off into a secondary thread. It’s easy to underestimate the subtleties of that, however, and there are good alternatives available. Here’s a simplified version of what I eventually elected to use. First the .h file:

@interface AsynchDownload : NSObject {
   NSURLConnection* conn;
   NSMutableData* data;
   id delegate;
}

- (id)initWithURL:(NSURL*)theURL delegate:(id)theDelegate;

- (void)cancel;

@end

and here the .m file:

@implementation AsynchDownload

- (id)initWithURL:(NSURL*)theURL delegate:(id)theDelegate {
   if ((self = [super init])) {
      delegate = theDelegate;
      data = nil;
      conn = [[NSURLConnection alloc]
         initWithRequest:[NSURLRequest requestWithURL:theURL]
         cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60.0]                            
         delegate:self];
   }
   return self;
}

- (void)cancel {
   [conn cancel];
}

- (void)dealloc {
   [conn release];
   [data release];
   [super dealloc];
}

- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error {
   [delegate performSelectorOnMainThread:@selector(failure:)
      withObject:[error localizedDescription] waitUntilDone:NO];
}

- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response {
   [data release];
   data = nil;
}

- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)theData {
   if (!data) {
      data = [[NSMutableData alloc] init];
   }
   [data appendData:theData];
}

- (void)connectionDidFinishLoading:(NSURLConnection*)connection {
   [delegate performSelectorOnMainThread:@selector(success:)
      withObject:data waitUntilDone:NO];
}

@end

and, finally, here’s how to call it:

   AsynchDownload* download = [[AsynchDownload alloc] initWithURL:theURL delegate:self];

… and in the same class, two methods:

- (void)failure:(NSString*)failure {
   [download release];
   //.. complain and show the failure string
}

- (void)success:(NSData*)data {
   [download release];
   // do something with data
}

… and finally, you might call

   [download cancel];

somewhere inside, say, a cancel button’s action.

This takes care of nearly all common cases. The only non-obvious part is the connection:didReceiveResponse: method; according to the docs, you need to discard already-received data there, as it may be sent multiple times before you get the final data. In this method, you could also ask the NSURLResponse for the data size, to set a download progress indicator.

Finally, note the performSelectorOnMainThread: messages which pass the results back to your own code; they neatly insulate you from threading effects.

Cocoa musings pt.3

No comments

It’s been a while since the last post, but I’ve finally gotten past the boring parts of the new Klicko System Preference pane & background process. The latter is actually a per-user Launch Agent. I’d used launchd before in the last Quay implementation, but didn’t fully understand what I was doing.

Well, I still can’t claim I fully understand all, but I’m definitely getting better at this. As soon as Klicko 1.2 is out and working (hm, maybe that should be the other way around?) I’ll back-port everything I learned to Quay.

One important part of messing around with launchd agents is that you have to set everything up just so – permissions of the executable and of its containing folders, as well as of the controlling plist if it’s a global agent; and you have to run launchctl to load (register) the controlling plist with launchd. Only then, and if the conditions inside the plist are satisfied, launchd will run your agent for you.

Uninstalling your agent, usually for putting an updated version in its place, must also be done carefully. You have to unload it if it’s loaded (even though it may not be running at the time) before swapping in the new executable and plist. Ideally, to avoid wasting time, there should be an easy way to test if your agent is properly loaded – the user might have unloaded it manually, or whatever. Unfortunately, the only “official” way to do so is to run launchctl again, passing in the “list” parameter, and parsing the output; not ideal.

However, there’s a primitive API in place in <launch.h>, and in fact launchctl uses this API itself to communicate with launchd. Still, there’s no documentation about it; only some sample code. Also, there’s this recent reply on the launchd-dev mailing list:

Right now the <launch.h> API is only rated for
daemons checking in with launchd (ala SampleD).
It is not really designed for job management.
Rather, we recommend that folks do their job
management by fork/exec of launchctl.

Quinn “The Eskimo!”

Still, I thought it would be interesting to use this API to check on the agent; after all, if something fails you’ll get an error back, and nothing will be messed up. So here’s the code for doing so:

#include <launch.h>

static id GetFromLaunchData(launch_data_t obj);

static void Launch_data_iterate(launch_data_t obj, const char *key, void* dict) {

   if (obj) {
      id value = GetFromLaunchData(obj);
      if(value) {
         [(NSMutableDictionary*)dict setObject:value forKey:[NSString stringWithUTF8String:key]];
      }
   }
}

static NSDictionary* GetFromLaunchDictionary(launch_data_t dict) {
   NSMutableDictionary* result = NULL;
   if (launch_data_get_type(dict)==LAUNCH_DATA_DICTIONARY) {
      result = [NSMutableDictionary dictionary];
      launch_data_dict_iterate(dict, Launch_data_iterate, result);
   }
   return result;
}

static NSArray* GetFromLaunchArray(launch_data_t arr) {
   NSMutableArray* result = NULL;   
   if (launch_data_get_type(arr)==LAUNCH_DATA_ARRAY) {
      size_t count = launch_data_array_get_count(arr);
      result = [NSMutableArray arrayWithCapacity:count];
      for (size_t i=0;i<count;i++) {
         id obj = GetFromLaunchData(launch_data_array_get_index(arr, i));
         if (obj) {
            [result addObject:obj];
         }
      }
   }
   return result;
}

static id GetFromLaunchData(launch_data_t obj) {
   switch (launch_data_get_type(obj)) {
   case LAUNCH_DATA_STRING:
      return [NSString stringWithUTF8String:launch_data_get_string(obj)];
   case LAUNCH_DATA_INTEGER:
      return [NSNumber numberWithLongLong:launch_data_get_integer(obj)];
   case LAUNCH_DATA_REAL:
      return [NSNumber numberWithDouble:launch_data_get_real(obj)];
   case LAUNCH_DATA_BOOL:
      return [NSNumber numberWithBool:launch_data_get_bool(obj)?YES:NO];
   case LAUNCH_DATA_ARRAY:
      return GetFromLaunchArray(obj);
   case LAUNCH_DATA_DICTIONARY:
      return GetFromLaunchDictionary(obj);
   case LAUNCH_DATA_FD:
      return [NSNumber numberWithInt:launch_data_get_fd(obj)];
   case LAUNCH_DATA_MACHPORT:
      return [NSNumber numberWithInt:launch_data_get_machport(obj)];
   default:
      break;
   }
   return nil;
}

static NSDictionary* GetFromJobLabel(NSString* job) {
   NSDictionary* result = nil;
   launch_data_t msg = launch_data_alloc(LAUNCH_DATA_DICTIONARY);
   if (msg&&launch_data_dict_insert(msg, launch_data_new_string([job fileSystemRepresentation]), LAUNCH_KEY_GETJOB)) {
      launch_data_t response = launch_msg(msg);
      launch_data_free(msg);
      if (response&&(launch_data_get_type(response)==LAUNCH_DATA_DICTIONARY)) {
         result = GetFromLaunchDictionary(response);
      }
      launch_data_free(response);
   }
   return result;
}

To use it, just call the last function like this:

   NSDictionary* dictionary = GetFromJobLabel(@"com.yourcompany.agentlabel");

and examine the dictionary. If it’s nil, some error happened, or your agent isn’t loaded into launchd.

If you do get a dictionary back, it will be autoreleased, and the “Label” key’s object should be equal to the string you passed in. There are two more keys that might be interesting to check: “PID” will contain your agent’s pid if it’s running, and will be missing if it’s not; and “LastExitStatus” will show you what your agent’s main() returned on its last run.

Note that most of the code above is actually based on the launchd source, converted to use Foundation objects instead of CFTypes.

Cocoa musings pt.2

No comments

One of the nice things Objective-C inherited from C is the switch statement. If you have set different tags in all your menu items, you often find you can do things like:

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   switch ([menuItem action]) {
   case 1:
      ...
   case 2:
      ...
   }
   return YES;
}

Those tags have to be set in Interface Builder, and you probably will have an enum somewhere to use symbolic constants instead. However, the standard C switch() is restricted to integer values; you can’t switch on strings or selectors. So, if you don’t want tags and need to switch on the menu item’s selectors, you’ll have to do:


- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   SEL action = [menuItem action];
   if (action==@selector(firstAction:)) {
      ...
   } else if (action==@selector(secondAction:)) {
      ...
   } else...
   return YES;
}

Bearable if you have two, or even four or five cases, but what if there are dozens? Or suppose you have to compare strings instead of selectors:

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   NSString* title = [menuItem title];
   if ([title isEqualToString:@"firstTitle"]) {
      ...
   } else if ([title isEqualToString:@"secondTitle"]) {
      ...
   } else...
   return YES;
}

Not that it’s recommendable, in practice, to compare menu item titles, but it’s a good example.
Well, there are other ways to make this more readable, or even more efficient. But here’s one neat way to convert a group of strings into integers for use in a switch(). First, let’s write an utility C function to do so:

NSUInteger CaseCodeForString(NSString* string) {
   static NSArray* array = nil;
   if (!array) {
      array = [[NSArray alloc] initWithObjects:
               @"zeroth string",
               @"first string",
               @"second string",
               @"third string",
               ...
            nil];
   }
   return [array indexOfObject:string];
}

Note the standard lazy allocate-once trick of declaring array static, initialize it to nil, and test before using. Anyway, this function will return 0 if you feed it @”zeroth string”, 1 for @”first string” and so forth… and return NSNotFound if the string isn’t in the array. So you could, in our last example, do:

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
   switch (CaseCodeForString([menuItem title])) {
   case 0:
      ...
   case 1:
      ...
   }
   return YES;
}

If there are many strings, this will be faster than a series of isEqualToString: calls; this is because NSArray uses a hash table to find the index of a particular object, and only goes into the actual string comparison if the two string’s -hash methods return the same value.

Re: Cocoa musings pt.1

No comments

In my post on event taps, I mentioned the following code to get a global event tap:

   CFMachPortRef tapg = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

This taps the event stream at the “annotated session” point (hence the kCGAnnotatedSessionEventTap parameter). Basically, this means that the event has already been analyzed as to destination application, and you can ask the passed event directly for that application’s process ID. This is was I was using in Klicko.

Unfortunately the docs don’t mention another step that takes place while “annotating” a mouse-down event: apparently, if the click was on a window’s title bar (not elsewhere in the window), the owning process is brought to the front before the tap sees the event! Klicko’s processing varied depending on the clicked-on window’s and the owning process’ state, so I was seeing different results depending on the click location – title bar or inside the window. Worse, a workaround I needed to do to bring non-main windows to the front for background processes wouldn’t be applied at all if the user clicked on a title bar!

A more subtle consequence was that I was intercepting (and discarding) mouse clicks, while the system was expecting the click that brought the process to the front to actually arrive at the process… the result was that, while the menu bar changed to the process, its windows would remain in the back.

After beating my head against this wall for several days, I was rereading the CGEvent docs again for ideas and noticed that I hadn’t tried applying a global tap before event annotation:

   CFMachPortRef tapg = CGEventTapCreate(kCGSessionEventTap, kCGEventTapOptionDefault, 
      CGEventMaskBit(kCGEventLeftMouseDown)|CGEventMaskBit(kCGEventLeftMouseUp),
      ProcessEvent,NULL);

Note the kCGSessionEventTap parameter. This meant that I had to discover the owning application’s process ID myself (instead of asking the mouse-down event directly). Premature optimization strikes again; I’d selected the annotated tap just to avoid doing these steps.

At any rate, I immediately discovered that I now could properly intercept clicks anywhere, discard them, and do process and window activation as I wanted to. Well, almost; there was still some redundant window activation to do, as Carbon and Cocoa apps seem to still have some residual differences in that regard. I’ll be filing bugs at Apple about some of these things.

Just saw Rob Keniger’s Wrapster plug-in for Coda, an interesting use of event taps; it’s a plug-in that intercepts key events for its master application.

The version I looked at (1.2) uses a global tap and checks if its master application is active:

   if(![NSApp isActive]) {
      return event;
   }

Of course, it would be conceptually more reliable to obtain the event’s destination process PSN and test against that; but the best way would be to tap the current process like this:

   ProcessSerialNumber psn = {kNoProcess, kCurrentProcess};
   CFMachPortRef tapg = CGEventTapCreateForPSN(&psn, kCGTailAppendEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyDown), ProcessEvent, NULL);

Global event taps are a great tool, but they’re also easy to abuse, and I suppose that in a year or two we might have dozens of utilities or plug-ins tapping events; the actual results might be dependent on the order they run or load, for instance, and overall responsiveness might suffer. Therefore, please make absolutely sure that you need a global tap to do whatever you want to do, that it takes as little time as possible, and that it does careful parameter checking.

One great application to test event taps (and to check their presence) is Event Tap Testbench. Its author, Bill Cheeseman, has helped me a lot with subtle details of both event taps and the Accessibility APIs; thanks Bill! Do also check out their other products.

Rob tests if Accessibility is enabled by trying to create a tap and seeing if it succeeds. While this works, I think it’s easier to do it like this:

   if (!AXAPIEnabled()&&!AXIsProcessTrusted()) {
      // error dialog here
   }

The first call checks if Accessibility is enabled in System Preferences; the second checks if your process is allowed to set a key tap even with Accessibility disabled. To achieve this, you must (from a process running as root) call AXMakeProcessTrusted() on the tapping application’s executable – this will take effect on its next run.

In the case of Klicko, it taps only mouse button events, for which Accessibility doesn’t need to be on; however, it uses the Accessibility APIs to check out details about the clicked-on windows. Some people dislike turning Accessibility on in System Preferences, so a future version of Klicko might use AXMakeProcessTrusted() to avoid this; this means asking the user for an Administrator password and running a separate tool, like Quay does. I’m still evaluating the trade-offs for that: it precludes running Klicko from ~/Applications or from the disk image, adds to the application size, and the user has to navigate an extra dialog on first run.

Update:Klicko now uses AXMakeProcessTrusted().

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.