20100729

Installing a Helper Tool

If your application does something with/to files that the user wouldn't normally have access to, you can use IPC (inter-process communication) with an Elevated Helper Tool. But how do you install the Helper Tool?

Prior to OS X 10.6, you would have to use a Helper Tool Installer, which you would have to launch using AuthorizationExecuteWithPrivileges(...). The (authorized) Installer would also have to install a launchd plist and register the plist using launchctl.

On 10.6 "Snow Leopard", Apple has made this process easier with the Service Management APIs. There is one API now, SMJobBless, that takes your authorization and the name of the helper tool, and installs it. Here are some of the details of using it.

Both the application and the Helper Tool must be code signed. It isn't necessarily important who is the CA.

The Helper Tool must reside inside of your application bundle in the Contents/Library/LaunchServices directory. The name of the HelperTool executable file MUST be its launchd Label. This is usually a reverse-DNS label such as com.QuantumCheese.MyApp.HelperTool .

Furthermore, SMJobBless requires two custom sections in the __TEXT segment of your executable. The first of these is the info plist. This is a normal Info.plist file, customized for your Helper Tool. It must contain an array SMAuthorizedClients of code signing requirements for authorized clients. It is also a good idea to have a version string (the key is CFBundleVersion) and a bundle name (key CFBundleIdentifier) (probably the Label).
The second is the launchd plist. This is the same plist you would have written from the Installer executable; it ends up in /Library/LaunchDaemons. See man (5) launchd.plist for information on this file; it must include the Label, and it should not include Program or ProgramArguments keys (they will be ignored and replaced).

In the Xcode project settings for your helper tool's target, add Other Linker Flags of
-sectcreate __TEXT __info_plist /path/to/your/daemon's/info.plist
and
-sectcreate __TEXT __launchd_plist /path/to/your/daemon's/launchd.plist
Note that these two lines will be split into eight lines; make sure that each 4 stay together.

The documentation for SMJobBless says that the only domain currently supported is kSMDomainSystemLaunchd. Therefore, SMJobBless will take your Helper Tool, extract its __launchd_plist to /Library/LaunchDaemons/reverse-DNS-HelperTool.plist, and copy the Helper Tool to /Library/PrivilegedHelperTools/reverse-DNS-HelperTool. It automatically version checks, and the API returns true on successful upgrade or if the tool is already installed.

Experiments indicate that it will return true even if a newer version of the Helper Tool is installed. Therefore, if your IPC demands that your bundled version exactly matches the installed version, you will still need to use the Helper Tool Installer to downgrade.

Even though it is tempting, I recommend against putting an additional copy of the executable into the Contents/MacOSX folder. While doing so would let you use the CFBundleCopyAuxiliaryExecutableURL API to create a URL to the helper executable, it increases the size of your bundle, which is a bigger problem than finagling the helper executable's URL on 10.5.
Instead, I recommend appending the Contents/Library/LaunchServices/HelperTool path to the URL obtained from CFBundleCopyBundleURL(CFBundleGetMainbundle()).

2 comments:

  1. Nice, this was exactly what I was looking for. The only thing left to figure out for me is how to detect if installed version of tool is the same as my eventually updated app has, so the user doesn't have to authorize on each and every launch.

    ReplyDelete
  2. You can avoid prompting unnecessarily by comparing the installed version against the version in the app bundle.

    Since we know that SMJobBless installs the helper executable in /Library/PrivilegedHelperTools, you can create an NSURL (CFURLRef) to /Library/PrivilegedHelperTools/com.yourdomain.yourapp.helper and use NSBundle's + bundleWithURL: method and then call - infoDictionary (or similarly use CFBundleCopyInfoDictionaryForURL) to get the helper tool's info dict. You can then query the dictionary for its version, and decide whether to prompt for blessing rights.

    ReplyDelete