Blog

Magic Launch 1.3: The power of shell commands

A new month, a new feature for Magic Launch. In this month’s episode, witness the incredible flexibility of mapping file type to shell commands (instead of mere applications). This proves the immense superiority of the Mac platform… Wait! Crap. Linux and Windows have that feature too, built-in, and since forever. Anyway, here’s how it works on a Mac with Magic Launch.

Settings for opening files in the command-line text editor Emacs.

I think the above picture summarizes well: you can write a command of your choice, a double-hash “##” in the command is replaced by the full path of the file, and you can choose between running it a terminal window, or run it faceless (useful to start X11 apps).

A few more details: the command is executed in your home directory using your default shell (bash if you haven’t changed anything), and the double-hash substitution works mostly like for $shell_variables:

  • If the double-hash is not quoted, it gets quoted if it contains spaces or other irregular characters.
  • If the double-hash is inside double quotes, special characters gets escaped as necessary (based on bash syntax).
  • If the double-hash is inside simple quotes, no substitution occurs.
  • Escaped hashes — those prefixed with a backslash — such as “#” cannot take part in a substitution.
Command As executed for “/a file.txt”
emacs ## emacs '/a file.txt'
emacs "##" emacs "/a file.txt"
emacs '##' emacs ##
emacs ## emacs ##

Version 1.3 also adds a new upgrade tweak. If you’re upgrading the Magic Launch by double-clicking the new file and having System Preference install it, you’ll see it quit and relaunch before opening the preference pane. Don’t panic, this is on purpose. This inconvenience is necessary for Magic Launch to work correctly when opening it the first time after the upgrade, all this thanks to a bug in System Preferences.


The Preference Pane Update Problem

The NSBundle class in Cocoa is pretty nice. It’s an easy interface to access everything in a bundle, from Info.plist keys to localized strings, contained resources to loading executable code. It’s nice, but there is an issue with how it works that will cause problems whenever a user updates a preference pane by double-clicking a new version and have System Preference install it over the older one.

The symptoms

Try this experiment: take any non-Apple preference pane out there, install it, then open the content of the installed package and delete its main nib file (or all nib files if you want). The next time you open it, you’ll see a blank preference pane, perhaps with a few errors on the console or more drastic problems (as you should expect after breaking it).

Now say you want to reinstall the non-broken version of this preference pane. You double-click on it, System Preferences asks you to if you want to replace the current version with the new one, and click yes. Once it installed the new preference pane, System Preferences opens it and you’re presented with… exactly the same blank pane as before (!).

Surprised? If you quit then reopen System Preference and open your panel, the new version will now work flawlessly. “Good, looks like it was just a fluke!” But in reality, every time you redo those steps the same events happens. What’s the matter?

And the root of the problem is… NSBundle!

Here is my understanding of what’s happening. System Preferences when it launches checks for every preference pane installed in its usual directories. It creates an instance of NSBundle for each of them to get their localized name and icon. That’s a good practice, as the Bundle Programming Guide tells you:

However, if you plan to retrieve more than one resource file, it is always faster to use a bundle object. Bundle objects cache search information as they go, so subsequent searches are usually faster.

Upon upgrading a new preference pane, it replaces the old bundle with the new one. This creates a problem: all the cached information from the old bundle is still kept in the NSBundle instance it created at launch time, making it out of sync with the actual content of the bundle.

The NSBundle instance your preference pane receives through its initWithBundle: method is thus contaminated by this outdated cached information. At this point, any use of this NSBundle instance to access the bundle’s content may fail or return outdated data, or work fine depending on whatever was present in the old version of the preference pane and the nature of the requested content.

Initially, I thought this was limited to returning the old version of the infoDictionary, because I was checking the version number in Magic Launch and displaying it in the panel, making that pretty obvious. But while working on bigger changes to the preference pane, I noticed the problem was more serious and applied to basically everything accessed through NSBundle.

Attempt at a workaround

Basically, all we need is a way to clear NSBundle’s cache. Unfortunately, there is no API to do that. Instead, let’s try create a new instance:

- (id)initWithBundle:(NSBundle *)bundle {
    NSBundle *newBundle = [NSBundle bundleWithPath:[bundle bundlePath]];
    [super initWithBundle:newBundle];
}

Looks right, but doesn’t work. NSBundle keeps track of all its instances and bundleWithPath: will return the same instance if one already leads to the same path. So we end up getting the same instance as before, with the same cached data and the same problems. Not good.

One could try to load every resource directly by path, bypassing NSBundle cache, but this soon become tedious, and in my case I have code shared between the Magic Launch Agent application and the preference pane that depends on NSBundle.

A similar but somewhat better solution would be to create a NSBundle wrapper class that would implement its own logic for retrieving resources and everything, but could be passed to the rest of Cocoa as an authentic NSBundle thanks to Objective-C dynamic binding. It’s rather hard work to duplicate everything in NSBundle, and then you have to forward any call to unimplemented method to the wrapped NSBundle for private methods or categories Apple might have in there to work.

That second solution would work, but I’m not feeling like writing all that code at the risk of introducing hard to notice bugs, and possibly forward-compatibility problems, just to fix the first landing after an upgrade done incorrectly by System Preferences.

The killer solution

The only acceptable solution I’ve found is this one: detect a version mismatch between the NSBundle instance and the actual code of the preference pane, and when there is a mismatch, just kill System Preferences and restart it. That’s rather drastic, I know, but it’s still better than having a half working, possibly buggy preference pane when first opening it after an upgrade.

What the user sees is the System Preference window momentarily disappearing, then reappearing and instantly switching to the newly upgraded preference pane.

It’s implemented like this:

- (id)initWithBundle:(NSBundle *)bundle {
    static NSString *const expectedVersion = @"1.3";
    NSString *bundleVersion = [[bundle infoDictionary] 
      objectForKey:(NSString *)kCFBundleVersionKey];

    if (![bundleVersion isEqual:expectedVersion]) {
        NSLog(@"Magic Launch version mismatch, restarting System Preferences...");
        MLReopenPreferences(bundle);
        exit(0);
    }
}

(I’ll leave the implementation of MLReopenPreferences above for a future post.)

What’s nice about this solution is that it’s forward compatible: if/when NSBundle or System Preferences get fixed, there will no longer be a version mismatch, and no version mismatch means no more killing of System Preferences.


A reconciling proposal

Some seem to think Apple’s section 3.3.1 is there to ban meta-platforms from iPhone OS. But do Apple want to ban all meta-platforms? Or do they just want to avoid meta-platforms that would put some other vendor in control? Those two things are very different, and the later is much more reasonable. If the fear is that Apple could become dependent on some other vendor (such as Adobe), they could do a much better job at writing the terms of the agreement…

I think Apple should just force any such translation layer or interpreter used in an application to be publicly available as open-source. If one such meta-platform ever becomes a problem, it’s easy for Apple to investigate the problem, and they can even release a fixed version themselves. It doesn’t solve all the problems people have with rule 3.3.1, but at least it’s not an outright ban of technology, and it even promotes sharing your building blocks with other developers (a good thing for the platform if you ask me).

And if that puts pressure on Adobe to release Flash as open-source, I’ll just say great!

Banning technology is regressive. Banning certain undesirable uses of technology (like lock-ins) is harder to argue with. I hope Apple take notice.


3.3.9

Michael Tsai, in conclusion of his post about the implications of section 3.3.1 of the new iPhone Developer Agreement, asks “What will the next rule change be?” That did’t take long. MacStories has now exposed section 3.3.9, which bans collection of statistical data.

  • All use of User Data collected or obtained through an Application must be limited to the same purpose as necessary to provide services or functionality for such Application. For example, the use of User Data collected on and used in a social networking Application could be used for the same purpose on the website version of that Application; however, the use of location-based User Data for enabling targeted advertising in an Application is prohibited unless targeted advertising is the purpose of such Application (e.g., a geo-location coupon application).

  • You may only provide or disclose User Data to third parties as necessary for providing services or functionality for the Application that collected the User Data, and then only if You receive express user consent. For example, if Your Application would like to post a message from a user to a third party social networking site, then You may only share the message if the user has explicitly indicated an intention to share it by clicking or selecting a button or checking a box that clearly explains how the message will be shared.

  • Notwithstanding anything else in this Agreement, Device Data may not be provided or disclosed to a third party without Apple’s prior written consent. Accordingly, the use of third party software in Your Application to collect and send Device Data to a third party for processing or analysis is expressly prohibited.

It looks good for privacy. Basically, it forbids developers from collecting statistics about the habits of their users, unless the software has a legitimate need to communicate some info to the outside.

It also forbids communication of “device data” to a third party. My interpretation of this is that a developer can collect statistics about the OS version and other such things (only when the application needs communicating with the developer’s server for legitimate reasons), but must not disclose those statistics to anyone.

I’m no fan of website, applications, or advertising firms collecting data about my habits without my consent, so in a way I can’t be against rules improving privacy. However, I find that the non-disclosure of device data goes a little too far: there’s nothing wrong about communicating interesting statistics.

Unfortunately, my guess is that Apple won’t hold itself to the same standards (Apple is not a third-party in this agreement). If an application uses Apple’s iAds, most likely you’ll get targeted advertising based on your current location, and data will be collected by Apple about your habits. This data can also easily be linked to your iTunes account. In some way, it’s worse for privacy: as developers switch to iAds all this information will be concentrated in one company’s hands: Apple.

AdMob (now owned by Google) and other mobile advertising services (MediaLets, MobClix) are going to be most affected by this change. While it doesn’t prevent others from offering advertisement services, the value of their ads will be severely crippled by these rules.

And obviously, third party analytic services (Flurry, SimpleGeo) are now banned from the iPhone. It’s possible that analytics becomes part of iAds, but I’m sure Apple will not share this data very easily.



  • © 2003–2024 Michel Fortin.