Yet Another [à compléter]

MacDoc: The Interesting Bits

Presentation

Macdoc is the new Mono API documentation browser build entirely with MonoMac (Cocoa bindings for .NET). It has been recently shipped as part of the latest MonoDevelop beta for Mac users where it replace the excellent GTK+ version we use everywhere else.

It was my first time hacking on any MonoMac app and along the way I came up with a couple of pieces of code that, I think, could be used as general recipes for MonoMac development.

Apple docs being severely lacking (or plainly useless) in some aspect of Mac development, some recipes also covers some general Mac constructs. MacDoc being a NSDocument-based application, recipes are given with respect to that style of Mac coding.

So, among the menu today we have:

  • Answering Open URL commands
  • Escalating privileges
  • Uncompressing .xar archives
  • Fighting WebView or how to handle image requests yourself
  • Redirecting printing to a WebView in a NSDocument application

Answering Open URL commands

Any Mac application after being started can receive an number of external signal called Apple events that are sent by other processes to ask the application to do something.

One of these Apple Events called GURL (the four chars code for “Get URL”) is a way for the application to receive URL request it can open from the outside world. So, for instance in MacDoc case, we respond to mdoc:// and monodoc:// that MonoDevelop send to update the documentation page we are showing to the user.

To answer these Apple events, there are a number of existing API. One of them is Carbon (part of the low level suite of Apple API) and an example of its usage can be seen in MonoDevelop and the GTK+ version of MonoDoc (e.g. MacInterop folder in MonoDevelop Mac addin).

But more interesting in the context of Cocoa application, there is a Objective-C wrapper called NSAppleEventManager around these C calls that is available as part of MonoMac master in the Foundation namespace.

If we return to our “Get URL” example, to let the system know that you can handle certain types of URL, you first need to set up some lines in your application Info.plist file. This will let tools like open (and more generally anything using the Launch Service API) automatically load your application when it’s used with an URL your are able to open. In MacDoc case, I paste below the relevant Info.plist lines for the two mdoc:// and monodoc:// URL schemes we support:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
        <key>CFBundleURLIconFile</key>
        <string>monodoc</string>
        <key>CFBundleURLName</key>
        <string>com.xamarin.monodoc.url</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>monodoc</string>
            <string>mdoc</string>
        </array>
    </dict>
</array>

Pretty straightforward.

Now to catch an event, we decorate a callback method with the [Export] attribute and an Objective-C selector name. In MacDoc case we define the handler this way:

[Export ("handleGetURLEvent:withReplyEvent:")]
public void HandleGetURLEvent (NSAppleEventDescriptor evt,
                               NSAppleEventDescriptor replyEvt)

I will show in a minute how you can process the arguments of this callback to extract the URLs. For now, let’s see how we register this callback with the event system via the NSAppleEventManager class:

public override void WillFinishLaunching (NSNotification notification)
{
    var selector = new MonoMac.ObjCRuntime.Selector ("handleGetURLEvent:withReplyEvent:");
    NSAppleEventManager.SharedAppleEventManager.SetEventHandler (this,
                                                                 selector,
                                                                 AEEventClass.Internet,
                                                                 AEEventID.GetUrl);
}

As you can notice, the call to NSAppleEventManager is made inside the NSApplicationDelegate.WillFinishLaunching override. In the Cocoa application startup cycle, this method is called when all default handler have been setup but no event has been processed yet making it a good candidate to register our own event handler.

Now, let’s see how we can extract the received URLs from the event data. Apple events can have complex data attached them ranging from simple number/string to nested list of them.

For the “Get URL” event, the URLs are stored in a simple list of string which you can process in normal for loop:

[Export ("handleGetURLEvent:withReplyEvent:")]
public void HandleGetURLEvent (NSAppleEventDescriptor evt, NSAppleEventDescriptor replyEvt)
{
	NSError error;

	// Received event is a list (1-based) of URL strings
	for (int i = 1; i &lt;= evt.NumberOfItems; i++) {
		var innerDesc = evt.DescriptorAtIndex (i);
		var url = new NSUrl (innerDesc.StringValue);
		// Do something with URL
	}
}

As you may have noticed, elements index inside a list are 1-based and not 0-based like we are used to.

Escalating privileges

Mac OS X being a UNIX operating system, it has a clear separation of privileges based on the traditional user mode system where root is the only account that can do anything on the system.

Although most applications are just fine running under a normal user, you may sometimes need to escalate privileges if at some point you want to, for instance, write files to a protected system directory.

There are two ways to do so, either upgrade the running process to a better user or launch an external process with better privileges than the calling one. Apple preferred way seems to be the second option (although they won’t really like an app that needs to be fully run as root in their Store anyway).

For Linux users, this is akin to using a sudo GUI (e.g. gksudo). Apple provides its own way to do the same thing with a C API of their own called SecurityFramework (and it’s Objective-C brother Security Foundation).

SecurityFramework, is a very much stupid low-level C API and although there is an Objective-C wrapper, it’s just an excuse for “Sorry we can’t do any better”.

Thankfully, we have the AuthorizationExecuteWithPrivileges shortcut call that make launching an external root process a bit easier. Be careful though because it was deprecated in Lion.

The following class and its LaunchExternalTool method let you start an external application as if run by the root user:

public static class RootLauncher
{
	const string SecurityFramework = "/System/Library/Frameworks/Security.framework/Versions/Current/Security";
		
	public static bool LaunchExternalTool (string toolPath)
	{
		IntPtr authReference = IntPtr.Zero;
		int result = AuthorizationCreate (IntPtr.Zero, IntPtr.Zero, 0, out authReference);
		if (result != 0) {
			Console.WriteLine ("Error while creating Auth Reference: {0}", result);
			return false;
		}
		AuthorizationExecuteWithPrivileges (authReference, toolPath, 0, new string[] { null }, IntPtr.Zero);
		return true;
	}
	
	[DllImport (SecurityFramework)]
	extern static int AuthorizationCreate (IntPtr autorizationRights,
	                                       IntPtr environment,
	                                       int authFlags,
	                                       out IntPtr authRef);
	
	[DllImport (SecurityFramework)]
	extern static int AuthorizationExecuteWithPrivileges (IntPtr authRef,
	                                                      string pathToTool,
	                                                      int authFlags,
	                                                      string[] args,
	                                                      IntPtr pipe);
}

Uncompressing .xar archives

Xar is an extensible archive format using an XML document to keep its inner filesystem information. It’s used by a lot of stuff distributed by Apple like, in MacDoc case, all their documentation bundles.

Xar is available by default on every Mac OS X installation and it has a very simple and straightforward API available in the libxar library making it really easy to bind.

In our case, we just needed to be able to decompress a Xar archive where we wanted which is easily achieved with the following class and its Extract method:

class XarApi
{
    public static bool Extract (string filename,
                                string outputDirectory,
                                CancellationToken token,
                                Action&lt;string&gt; pathCallback)
    {
        var cwdSave = Directory.GetCurrentDirectory ();
        Directory.SetCurrentDirectory (outputDirectory);
        IntPtr xart = IntPtr.Zero;
        IntPtr iter = IntPtr.Zero;

        try {
            xart = xar_open (filename, 0);
            iter = xar_iter_new ();

            IntPtr file = xar_file_first (xart, iter);
            while (file != IntPtr.Zero) {
                if (token.IsCancellationRequested)
                    return false;
                if (pathCallback != null)
                    pathCallback (xar_get_path (file));
                if (xar_extract (xart, file) &amp;lt; 0)
                    return false;
                file = xar_file_next (iter);
            }
        } finally {
            if (iter != IntPtr.Zero)
                xar_iter_free (iter);
            if (xart != IntPtr.Zero)
                xar_close (xart);
            Directory.SetCurrentDirectory (cwdSave);
        }

        return true;
    }

    [DllImport ("xar")]
    extern static IntPtr xar_open (string filename, int mode); // 0 for READ, 1 for READWRITE

    [DllImport ("xar")]
    extern static IntPtr xar_close (IntPtr xart);

    [DllImport ("xar")]
    extern static IntPtr xar_iter_new ();

    [DllImport ("xar")]
    extern static void xar_iter_free (IntPtr iter);

    [DllImport ("xar")]
    extern static IntPtr xar_file_first (IntPtr xart, IntPtr iter);

    [DllImport ("xar")]
    extern static IntPtr xar_file_next (IntPtr iter);

    [DllImport ("xar")]
    extern static int xar_extract (IntPtr xart, IntPtr filet);

    [DllImport ("xar")]
    extern static string xar_get_path (IntPtr file);
}

Process is pretty simple really, the only catch being that xar extract archive filesystem based on the current working directory which is why we adapt this setting to the given parameter. You thus need to be careful if for some reason you want to execute more than one call concurrently.

Fighting WebView or how to handle image requests yourself

WebView is the widget which can display any kind of rich HTML using Webkit. It has some integration with the embedder, letting it decide how the WebKit engine should handle some operation (e.g. link navigation). What it doesn’t let you do though is handle linked content request yourself i.e. any resource that needs to be downloaded separately (images, external scripts, , …).

In our case, we try as much as possible to display inlined HTML (including the CSS and our small bit of Javascript) inside the widget for our documentation and for default images we could certainly well extract them in a known place on disk and point the browser to it.

What we couldn’t act on however were the internal images referenced from documentation as WebView doesn’t let you catch Web request and feed on your own data. Thus we needed a way to trick the system to let us inject our own bytes at some point.

As I said earlier, for convenience we try to inline as much stuff as possible, so taking that reasoning to images, we needed to do the same thing for them. Enter the Data URI scheme which let you embed raw data (or well, base64 encoded data) directly as content.

What we can thus do with our WebView is to hack on the DOM when the page has loaded to detect places where a &lt;img /&gt; tag referencing an internal image resource is used and fill that tag with raw data taken from our store.

If we want to manipulate the DOM, we need it to be properly initialized. A way to do that is to hookup our method to the FinishedLoad event which tells when the DOM is ready for consumption for the currently loading document (remember that loading a page is asynchronous in a WebView).

The method itself which fetch image tags and fill them with our data is given below:

var dom = e.ForFrame.DomDocument;

var imgs = dom.GetElementsByTagName ("img")
              .Where (n =&gt; n.Attributes["src"].Value.StartsWith ("source-id"));
byte[] buffer = new byte[4096];

foreach (var img in imgs) {
    var src = img.Attributes["src"].Value;
    // Our specific call to get an embedded image stream
    var imgStream = AppDelegate.Root.GetImage (src);
    if (imgStream == null)
        continue;
    var length = imgStream.Read (buffer, 0, buffer.Length);
    var read = length;
    while (read != 0) {
        if (length == buffer.Length) {
            var oldBuffer = buffer;
            buffer = new byte[oldBuffer.Length * 2];
            Buffer.BlockCopy (oldBuffer, 0, buffer, 0, oldBuffer.Length);
        }
        length += read = imgStream.Read (buffer, length, buffer.Length - length);
    }

    var data = Convert.ToBase64String (buffer, 0, length, Base64FormattingOptions.None);
    var uri = "data:image/" + src.Substring (src.LastIndexOf ('.')) + ";base64," + data;
    ((DomElement)img).SetAttribute ("src", uri);
}

The two application-specific parts of this method are how we recognize a tag pointing to an embedded image (in our case their src attribute value is prefixed with “source-id”) and the call which get us a Stream for the image (here we get it from our documentation bundle). The rest is mostly stream reading boilerplate.

Redirecting printing to a WebView in a NSDocument application

Having a NSDocument-based application means a couple of standard operation are made easier for you to use. One of them is printing which simply require a couple of plumbing with most of the heavy lifting left to Cocoa.

What you may want to do however is to only print a part of your UI, generally the one that display the actual document content which is not something Cocoa can guess for you.

In MacDoc case, the WebView that show the API documentation is what we are interested in printing so let’s see how can redirect global print request to that specific part of the application.

First, we need to register an action on the Print menu item that points to our subclass of NSApplicationDelegate. In the action implementation, we will simply redirect the call to the currently activated document:

partial void HandlePrint (NSObject sender)
{
    NSDocumentController.SharedDocumentController.CurrentDocument.PrintDocument (sender);
}

Now by default, this will actually try to print the currently focused widget in your document UI which is most of time not something you want (feels weird to print a button).

As told earlier, most of the time what you rather want is to print a specific part of the UI. Fortunately, almost all widgets that display rich content also have an implementation of a print operation. WebView is no different and this the widget we are going to use as an example.

So, to redefine the printing behavior of a document, the first thing you need to do is to override the PrintOperation method in your NSDocument subclass.

Then, your simply fetch the current FrameView displayed by your WebView and proxy the print operation through it. This could be something like this:

public override NSPrintOperation PrintOperation (NSDictionary printSettings,
                                                 out NSError outError)
{
    outError = null;
    return webView.MainFrame.FrameView.GetPrintOperation (new NSPrintInfo (printSettings));
}

The given NSDictionary parameter is a raw representation of a NSPrintInfo so it’s enough to simply instantiate the later with the former. The returned print operation will contains what’s necessary to print the rendered HTML shown by the WebView.