MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Sweetening Your Xgrid with Cocoa

by Drew McCormack
09/13/2005

Last time, I took you through the changes that Xgrid has seen in Tiger, and showed you how to setup a private Xgrid controller and run simple jobs with the xgrid command-line tool. This time we'll take a look at XGridFoundation, a framework that allows you to integrate Xgrid into your Cocoa applications.

In the last installment, I made some grandiose promises about not only covering the Xgrid Cocoa API, but also incorporating some Automator and Core Image. When I got down to business, I realized the complexity of XGridFoundation meant that including these technologies would simply cloud the issues. So I'm going to keep it strictly Xgrid this time, and leave the other cool stuff for another occasion.

Between a Rock and XGridFoundation

There is something of a tradition amongst Cocoa tutorial writers to try and impress the reader with a full-featured application created from a bare minimum of source code. Want to write a web browser with a single line of code? How about a personal organizer with no code at all? If this is how you measure the quality of your Cocoa tutorials, I'm afraid you're about to be disappointed, because a line of code won't even get you in the front gate with XGridFoundation.

I have to admit that when I started working with XGridFoundation, I had expected it to be considerably easier going than it turned out to be. I have quite a few years of Cocoa developing experience, and I can't think of many corners of the frameworks that I have had to struggle with as much as XGridFoundation. It almost makes getting your teeth pulled seem like a weekend in Acapulco.

To give you a rough idea of what I am talking about, the simple application I will develop in this tutorial is equivalent to around three of the xgrid commands discussed last time, but requires in the vicinity of 500 lines of source code to achieve! There is definitely an opportunity for a third party to step in and write a simplified interface to XGridFoundation, and I know that some efforts in this direction are already underway.

To be fair, Xgrid deals with a particularly difficult part of programming: networking. Networking tends to be difficult, because everything happens asynchronously. Networking activities are slow, and it's unacceptable for a program to lock up while a request is made to a server. Network programming thus tends to be nonlinear, with the instigation of an action being followed some time later by a "callback," a bit like the echo that follows your shout in a large cavern.

Apple has given developers an all-areas pass with XGridFoundation. It is very powerful, but the learning curve is steep. The Xgrid documentation in Xcode is quite sparse, consisting of automatically-generated class descriptions, and although there are sample projects (in /Developer/Examples/Xgrid), they are fully-fledged applications, making it difficult to discern the forest for the trees. My objective here is to create one of the simplest Xgrid-enabled Cocoa applications imaginable, so that you get more forest and less tree.

Before I begin the tutorial proper, I need to send a big thanks to Charles Parnot, author of GridStuffer, and founder of the Xgrid@Stanford project. Charles probably knows more about XGridFoundation than anyone outside Apple, and he successfully helped the author avert a nasty outbreak of premature baldness during the background work for this tutorial.

Central Command

As I mentioned earlier, the application we are going to develop is about as simple an Xgrid-Cocoa app as you could imagine. The intention is to strip everything back to the most basic steps required to write an Xgrid client. The application, which is called "Central Command," is basically a simplified, graphical version of the command-line xgrid tool. You can download the Xcode project for Central Command here. To use it, you enter an Xgrid controller host name, an optional password, and a UNIX command, and then click the Run button. The command is submitted to the controller, which sends it to an Xgrid agent, where it is run. The output is returned via the controller to Central Command, which displays the results. It doesn't get much simpler than that!

The Central Command user interface.
Figure 1. The Central Command user interface

I'm going to focus on the aspects of Central Command that relate specifically to Xgrid, and not on the minutiae of developing a Cocoa application. I won't discuss how you use Interface Builder to create a user interface, for example, or how you connect up the elements of the interface to the attributes of classes using Cocoa Bindings. If these things are alien to you, you're probably already out of your depth. You'll need to have some Cocoa programming experience to grok what follows.

An XGridFoundation Class Overview

To use XGridFoundation, you simply add the framework, which is located in /System/Library/Frameworks, to your Xcode project. In any source file that needs access to XGridFoundation, you import the framework header, like this:

#import <XGridFoundation/XGridFoundation.h>

I want to kick off with an overview of the main players in the XGridFoundation framework. With most Cocoa frameworks (e.g. WebKit), you tend to only use a small portion of the available classes when you are developing an application. With XGridFoundation, you really will need just about all of them, even for something as simple as Central Command. So what follows is an overview of the classes, and what they do. The classes are presented approximately in the order you would encounter them in the flow of a program.

XGConnection
This is used to represent a connection to an Xgrid server. It can be initialized with a host name, or via Bonjour.
XGAuthenticator
In order to open a connection, you will often need a means of authenticating with the Xgrid server, such as a password. XGAuthenticator is an abstract class whose subclasses are used by an XGConnection to authenticate.
XGTwoWayRandomAuthenticator
This subclass of XGAuthenticator performs password authentication.
XGGSSAuthenticator
This subclass of XGAuthenticator authenticates with Single Sign-On.
XGController
Instances of this class are proxies for Xgrid controllers. They are initialized with an XGConnection, and are used to submit jobs.
XGActionMonitor
This class is used to monitor the activity of some asynchronous requests, such as submitting a job via an XGController.
XGResource
This abstract class represents remote-grid resources, like grids and jobs. Instances of subclasses of XGResource are proxies for entities on the Xgrid server.
XGGrid
This subclass of XGResource represents grids on the Xgrid controller.
XGJob
This subclass of XGResource represents jobs running on the Xgrid controller.
XGFile
This represents a file or stream that is stored on the Xgrid controller.
XGFileDownload
This is a class used to retrieve files and streams from the Xgrid controller after a job is complete.

Putting It All Together

So how do all of these classes work together in a running program? This is one area of the current Xgrid documentation that is severely lacking, so I will try to clarify things here.

As I mentioned earlier, what makes XGridFoundation hard is that everything revolves around networking, and that spells one thing: asynchronicity. To perform just about any operation with XGridFoundation, you have to request it via a method invocation, and then wait for a "callback," which could be a call to a delegate method, but more often than not, is simply a notification arising from key-value observing (KVO). In other words, to know when any stage of the process is complete, you either have to register a delegate, or add an observer with KVO.

To give you an idea of the basic steps involved in writing an Xgrid-enabled application, I am going to list the various operations undertaken by Central Command, in the order that they occur. I will delve into the source code of each step in the coming sections.

  1. The run method of the application controller class — not to be confused with the Xgrid controller — is called when the user presses the Run button. This method creates an XGTwoWayAuthenticator with the password entered by the user, and uses it (together with the host name entered) to initialize an XGConnection object. The delegate of the XGConnection is set to the application controller, and the asynchronous open method of XGConnection is invoked.
  2. The XGConnection tries to connect to the Xgrid server. If successful, it calls back to its delegate, invoking connectionDidOpen:.
  3. An XGController instance is created from the XGConnection. KVO is used to observe the state attribute of the XGController.
  4. When the XGController state attribute changes to XGResourceStateAvailable, a job specification dictionary is created, and the job initiated by calling the XGController method performSubmitJobActionWithJobSpecification:gridIdentifier:. This method returns an instance of XGActionMonitor; the outcome attribute of this action monitor is observed using KVO.
  5. If the XGActionMonitor outcome attribute changes to XGActionMonitorOutcomeSuccess, the job identifier is retrieved from the action monitor's results dictionary. The job identifier is used to retrieve the XGJob instance that represents the job on the Xgrid controller. The XGJob is extracted from an XGGrid object, which is retrieved from the XGController. (Got that? XGController begat XGGrid, which begat XGJob.) The state attribute of the XGJob is now observed with KVO. (This step is complicated by the fact that at the time that the job identifier becomes available, the XGJob may not yet have been added to the XGGrid. If this is the case, KVO is used to wait until the XGGrid is up-to-date.)
  6. When the XGJob's state changes to XGResourceStateFinished, the performGetOutputStreamsAction method of XGJob is called to start retrieving information about the command output. This method returns an XGActionMonitor, and KVO is used to observe its outcome attribute.
  7. When the outcome attribute changes to XGActionMonitorOutcomeSuccess, an array of XGFile objects is retrieved from the results dictionary of the XGActionMonitor. These XGFile objects encapsulate information about the output and error streams. They are used to initialize XGFileDownload objects, in order to retrieve the stream data from the Xgrid controller. The downloads' progress is monitored via XGFileDownload delegate methods.
  8. The data of each stream is accumulated during repeated calls to the XGFileDownload delegate method fileDownload:didReceiveData:. Each download is complete when the delegate method fileDownloadDidFinish: is invoked.

By now, you are probably wishing you had read the tutorial about building a personal organizer without touching your keyboard. But since your here anyway, why not read on and add to the confusion? In the coming sections I will dissect the steps above, showing you what it all means in Objective-C.

Opening a Connection

We'll begin in the run method of Central Command's Controller class. This method, which is invoked when the user hits the Run button, attempts to open a connection with the host name and password entered.

// Setup a password authenticator, if necessary
XGTwoWayRandomAuthenticator *authenticator = nil;
if ( usePasswordAuthentication ) {
    authenticator = [[[XGTwoWayRandomAuthenticator alloc] init] autorelease];
    [authenticator setUsername:@"one-xgrid-client"];
    [authenticator setPassword:[self controllerPassword]];
}

// Make a connection to the Xgrid server. 
XGConnection *newConnection = 
    [[[XGConnection alloc] initWithHostname:[self controllerHostname] portnumber:0] autorelease];
[newConnection setAuthenticator:authenticator];
[newConnection setDelegate:self];
[[self connection] setDelegate:nil]; // Set delegate of previous connection to nil
[self setConnection:newConnection]; // This registers as the connection's delegate

// Open connection
[newConnection open];

First, an XGTwoWayRandomAuthenticator is created, if password authentication is being used. The password is set with the setPassword: method, which may not come as a surprise, but the call to setUsername: could have you a little perplexed. (I know it had me perplexed...still does.) The username you should use in this call is "one-xgrid-client," for reasons that I have been unable to ascertain. Just mark it down to idiosyncrasy, and move on. I did.

The XGConnection is initialized with the hostname, and a port number of 0. If you pass 0 for the port, the default port will be used, which is what we want in this case. The setAuthenticator: method is used to supply the authenticator to the XGConnection; if nil is passed to this method, no authentication is used.

The delegate of the XGConnection is set to Controller instance. Note that if an XGConnection is already stored in the Controller, its delegate is set to nil to prevent future calls to the delegate methods. This pattern repeats itself throughout Central Command: when initiating some form of observation, whether it involves delegation or KVO, the Controller first stops observing objects leftover from earlier runs.

The last method invoked is open. This asynchronous method attempts to contact the host specified, and authenticate. The XGConnection then calls back to its delegate, the Controller, to report success or failure. The delegate methods look like this:

-(void)connectionDidNotOpen:(XGConnection *)connection 
        withError:(NSError *)error {
    [self setRunning:NO];
    NSAlert *alert = [NSAlert alertWithMessageText:@"Could not open connection to Xgrid server." 
        defaultButton:@"OK" alternateButton:nil otherButton:nil
        informativeTextWithFormat:
            @"Check that controller host name was entered correctly, and that it is "
            @"available."];
    [alert runModal];
}

-(void)connectionDidClose:(XGConnection *)conn {
    if ( [conn error] ) {
        int errorCode = [[connection error] code];
        NSAlert *alert;
        if ( errorCode == 530 || errorCode == 535 ) { // Authentication error
            alert = [NSAlert alertWithMessageText:@"An authentication error occurred." 
                defaultButton:@"OK" alternateButton:nil otherButton:nil
                informativeTextWithFormat:
                    @"Check that you have entered the authentication details "
                    @"correctly, and that you have used the correct authentication method."];
            [alert runModal];
        }
        else {
            alert = [NSAlert alertWithMessageText:@"Could not open connection to Xgrid server." 
                defaultButton:@"OK" alternateButton:nil otherButton:nil
                informativeTextWithFormat:
                    @"Check that controller host name was entered correctly, and that the server is "
                    @"available."];
        }
        [self setRunning:NO];
        [alert runModal];
    }
}

-(void)connectionDidOpen:(XGConnection *)conn {
    // Create controller proxy
    XGController *newController = [[[XGController alloc] initWithConnection:conn] autorelease];
    [self beginMonitoringController:newController];
}

Much of this code simply deals with the errors that can arise. Authentication errors, such as a misspelled password, are quite common. Rather surprisingly, if an authentication error occurs, the connectionDidClose: method is called, not connectionDidNotOpen:. The XGConnection includes a method called error that returns an NSError explaining what went wrong. You can retrieve an error code from the NSError; a value of 530 or 535 indicates an authentication error.

If all goes well, the connectionDidOpen: delegate method is called. It creates an XGController, initializing it with the newly created XGConnection. It then calls the method beginMonitoringController, which stops observing any previously used XGController, and begins observing the state attribute of the new XGController with KVO.

-(void)beginMonitoringController:(XGController *)newController {
    [[self controller] removeObserver:self forKeyPath:@"state"];
    [newController addObserver:self forKeyPath:@"state" options:0 context:NULL];
    [self setController:newController];
}

Submitting a Job

Whenever an object's attribute changes, and it is being observed with KVO, the observer's observeValueForKeyPath:ofObject:change:context: method is invoked. This method needs to identify which object is calling, and take appropriate action.

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
    change:(NSDictionary *)change 
    context:(void *)context {
    if ( object == [self controller] ) {
        [self controllerStateDidChange];
    }
    else if ( object == [self jobSubmissionMonitor] ) {
        [self jobSubmissionStateDidChange];
    }
    else if ( object == [self outputRetrievalMonitor] ) {
        [self outputRetrievalStateDidChange];
    }
    else if ( [object isKindOfClass:[XGJob class]] ) {
        [self jobStateDidChange:object];
    }
    else if ( [object isKindOfClass:[XGGrid class]] ) {
        [self gridStateDidChange:object];
    }
}

It may be tempting to include the code for dealing with the various state changes directly in this method, but in practice this leads to unreadable code. It is better to factorize your code as shown, keeping the observeValueForKeyPath:ofObject:change:context: simple, and calling other methods to do the dirty work.

When the XGController's state attribute changes, the controllerStateDidChange method is invoked. This simple little method checks if the new state of the XGController is equal to XGResourceStateAvailable, and if it is, submits a job by calling submitJob.

-(void)controllerStateDidChange {
    XGController *conn = [self controller];
    if ( [conn state] == XGResourceStateAvailable ) [self submitJob];
}

Submitting a job via XGridFoundation bears a strong resemblance to how it is done in batch mode with the xgrid command-line tool. You have to create a property list dictionary with the job specification. You can do this by reading in a property list file, or you can combine objects of property list classes like NSArray, NSString, and NSDictionary directly in the code. The latter approach is taken by Central Command.

-(void)submitJob {
    // Create the job specification
    NSArray *commandAndArgs = [jobCommand componentsSeparatedByString:@" "];
    NSString *command = [commandAndArgs objectAtIndex:0];
    NSArray *commandArgs = [commandAndArgs subarrayWithRange:NSMakeRange(1,[commandAndArgs count]-1)];
    NSDictionary *commandDict = 
        [NSDictionary dictionaryWithObjectsAndKeys:
            commandArgs,    XGJobSpecificationArgumentsKey, 
            command,        XGJobSpecificationCommandKey, nil];
    NSDictionary *tasksDict = 
        [NSDictionary dictionaryWithObject:commandDict forKey:@"0"];
    NSDictionary *jobSpecification = 
        [NSDictionary dictionaryWithObjectsAndKeys:
            @"com.maniacalextent.centralcommand",   XGJobSpecificationApplicationIdentifierKey,
            @"Central Command",                     XGJobSpecificationNameKey,
            tasksDict,                              XGJobSpecificationTaskSpecificationsKey,
            nil];
                
    // Submit job, and beginning monitoring.
    XGActionMonitor *newJobSubmissionMonitor = 
        [controller performSubmitJobActionWithJobSpecification:jobSpecification gridIdentifier:nil];
    [[self jobSubmissionMonitor] removeObserver:self forKeyPath:@"outcome"]; // Stop observing previous monitor
    [newJobSubmissionMonitor addObserver:self forKeyPath:@"outcome" options:0 context:NULL];
    [self setJobSubmissionMonitor:newJobSubmissionMonitor]; 
}

The first half of this method combines objects into a property list tree. If you were to write this out in property list format, it would look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">
<dict>
        <key>jobSpecification</key>
        <dict>
                <key>applicationIdentifier</key>
                <string>com.maniacalextent.centralcommand</string>
                <key>name</key>
                <string>Central Command</string>
                <key>taskSpecifications</key>
                <dict>
                        <key>0</key>
                        <dict>
                                <key>arguments</key>
                                <array>
                                        <string>3</string>
                                        <string>2005</string>
                                </array>
                                <key>command</key>
                                <string>/usr/bin/cal</string>
                        </dict>
                </dict>
        </dict>
</dict>
</plist>

The property list includes a cal command, but Central Command actually determines the command and its arguments at runtime from the entries made by the user, as you can see from the code above.

The second half of the submitJob method requests the XGController to submit a job with the specification given, by invoking performSubmitJobActionWithJobSpecification:gridIdentifier:. Setting the grid identifier to nil causes the Xgrid controller's default grid to be used. The performSubmitJobActionWithJobSpecification:gridIdentifier: returns an XGActionMonitor object, which is observed using KVO to determine when its outcome attribute changes. When this happens, observeValueForKeyPath:ofObject:change:context: invokes the jobSubmissionStateDidChange method.

The jobSubmissionStateDidChange method checks the value of the XGActionMonitor outcome attribute; if a failure occurred, it is reported to the user, and if the job submission was successful, it retrieves the job's identifier from the results dictionary of the action monitor, and invokes a method that begins to monitor the job's progress.

-(void)jobSubmissionStateDidChange {
    XGActionMonitor *monitor = [self jobSubmissionMonitor];
    NSAlert *alert;
    NSString *jobId;
    switch ( [monitor outcome] ) {
        case XGActionMonitorOutcomeFailure:
            [self setRunning:NO];
            alert = [NSAlert alertWithError:[monitor error]];
            [alert runModal];
            break;
        case XGActionMonitorOutcomeSuccess:
            jobId = [[monitor results] objectForKey:@"jobIdentifier"];
            [self setJobIdentifier:jobId];
            [self attemptToMonitorJob];
            break;
    }
}

The method attemptToMonitorJob may sound like it was christened when the developer was feeling particularly pessimistic. Afterall, why couldn't you just start monitoring the job, and forego the "attempt"? It turns out that even when a job is successfully submitted, the XGGrid in which it runs may not be fully up-to-date, and may not yet know of the existence of the job. The attemptToMonitorJob method is thus designed to be invoked multiple times, until conditions are such that the XGJob can be retrieved from the XGGrid:

-(void)attemptToMonitorJob {
    XGGrid *grid = [[self controller] defaultGrid];
    XGJob *job = [grid jobForIdentifier:[self jobIdentifier]];
    if ( nil == job ) { 
        // Job has not been added to the grid yet, so monitor the grid until it has
        [grid addObserver:self forKeyPath:@"jobs" options:0 context:NULL];
    }
    else {
        // Job is available from grid, so start observing it, and stop observing the grid
        [job addObserver:self forKeyPath:@"state" options:0 context:NULL];  
        [grid removeObserver:self forKeyPath:@"jobs"]; 
    }
}

If the XGJob is not yet associated with the default grid, the XGGrid method jobForIdentifier: will return nil. If this happens, KVO is used to monitor the state of the jobs attribute of the XGGrid, which is an array of the XGJob objects under the control of the grid. When the jobs array changes, observeValueForKeyPath:ofObject:change:context: reinvokes attemptToMonitorJob, to see if the desired XGJob has become available. It does this until jobForIdentifier returns a non-nil value, at which point KVO is used to start observing the XGJob's state attribute.

Retrieving Results

When the XGJob state changes, a call is made to jobStateDidChange: (via observeValueForKeyPath:ofObject:change:context:). If the job failed, the user is informed; if it succeeded, beginRetrievingOutputStreamsForJob: is invoked to start retrieving the job output.

-(void)jobStateDidChange:(XGJob *)job {
    NSAlert *alert;
    XGResourceState state = [job state];
    switch (state) {
        case XGResourceStateFailed:
            [self setRunning:NO];
            alert = [NSAlert alertWithMessageText:@"Job failed." 
                defaultButton:@"OK" alternateButton:nil otherButton:nil
                informativeTextWithFormat:@"Try running the job again."];
            [alert runModal];
            [job removeObserver:self forKeyPath:@"state"];       
            break;
        case XGResourceStateFinished:
            [self beginRetrievingOutputStreamsForJob:job];       
            break;
    }
}

The Controller method beginRetrievingOutputStreamsForJob: requests information about the output produced by the job from the XGJob object, using the performGetOutputStreamsAction.

-(void)beginRetrievingOutputStreamsForJob:(XGJob *)job {
    // Stop monitoring job
    [job removeObserver:self forKeyPath:@"state"];

    // Begin getting output streams
    XGActionMonitor *newOutputRetrievalMonitor = [job performGetOutputStreamsAction];
    [[self outputRetrievalMonitor] removeObserver:self forKeyPath:@"outcome"]; // Stop monitoring previous
    [newOutputRetrievalMonitor addObserver:self forKeyPath:@"outcome" options:0 context:NULL];
    [self setOutputRetrievalMonitor:newOutputRetrievalMonitor];
}

Despite what you might think, this does not start downloading the output data; it simply starts retrieving information about the data that is available from the Xgrid controller. Again, KVO is used on the outcome attribute of the XGActionMonitor returned by performGetOutputStreamsAction to determine when the information becomes available. outputRetrievalStateDidChange is invoked when it does.

-(void)outputRetrievalStateDidChange {
    XGActionMonitor *monitor = [self outputRetrievalMonitor];
    NSAlert *alert;
    NSArray *streams;
    switch ( [monitor outcome] ) {
        case (XGActionMonitorOutcomeFailure):
            // Report error
            [self setRunning:NO];
            alert = [NSAlert alertWithError:[monitor error]];
            [alert runModal];
            break;
        case (XGActionMonitorOutcomeSuccess):
            streams = [[monitor results] objectForKey:XGActionMonitorResultsOutputStreamsKey];
            [self beginFileDownloadsForOutputStreams:streams];
            break;
    }
}

If the action was successful, the results dictionary of the XGActionMonitor yields an array of XGFile objects, representing the output streams we seek to download. (You are probably now starting to recognize a commonly-used pattern in XGridFoundation: invoke performBlah; monitor outcome attribute of XGActionMonitor returned with KVO; retrieve information from XGActionMonitor results dictionary.)

The last step in the process is to retrieve the stream data represented by the XGFile objects from the Xgrid controller. This is initiated in beginFileDownloadsForOutputStreams: using the XGFileDownload class.

-(void)beginFileDownloadsForOutputStreams:(NSArray *)streams {
    // Start up file downloads for standard output and standard error, if they exist.
    NSEnumerator *streamEnum = [streams objectEnumerator];
    XGFile *stream;
    while (stream = [streamEnum nextObject]) {
        if ( [[stream path] isEqualToString:XGFileStandardOutputPath] ) {
            XGFileDownload *dl = 
                [[[XGFileDownload alloc] initWithFile:stream delegate:self] autorelease];
            [self setOutputFileDownload:dl];
            [self setJobOutput:@""];
        }
        else if ( [[stream path] isEqualToString:XGFileStandardErrorPath] ) {
            XGFileDownload *dl = 
                [[[XGFileDownload alloc] initWithFile:stream delegate:self] autorelease];
            [self setErrorFileDownload:dl];
            [self setJobError:@""];
        }
    }

    // If there are no streams, the command entered probably did not exist.
    // Warn the user.
    if ( [streams count] == 0 ) {
        NSAlert *alert = [NSAlert alertWithMessageText:@"The command does not exist." 
            defaultButton:@"OK" alternateButton:nil otherButton:nil 
            informativeTextWithFormat:@"Check the path of the command you entered."];
        [alert runModal];
        [self setRunning:NO];
    }
}

The path attribute of the XGFile objects is used to establish which streams are available. The string constants XGFileStandardOutputPath and XGFileStandardErrorPath identify standard output and standard error, respectively. For each stream, an XGFileDownload object is initialized with the corresponding XGFile, and its delegate set to the Controller. The downloads' progress is monitored via calls to delegate methods in this case.

Note the last part of beginFileDownloadsForOutputStreams:; it checks to see if the streams array is empty. If it is, it is assumed that the command entered by the user did not exist. Note that this seems to be the only means of establishing this — no error will arise from running the job. There also doesn't seem to be any way to retrieve the exit code of a command using XGridFoundation. Hopefully these teething problems will be ironed out in future releases.

There are several XGFileDownload delegate methods. You have a few choices in this regard. You can wait until the files are downloaded in full, and stored somewhere on the local hard disk, or you can process the incoming data a little at a time. Central Command takes the latter option, because for the small amounts of output data being generated, it seems silly to store it in a temporary file.

-(void)fileDownload:(XGFileDownload *)fileDownload didReceiveData:(NSData *)data {
    NSString *newString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    if ( fileDownload == [self outputFileDownload] ) {
        newString = [[self jobOutput] stringByAppendingString:newString];
        [self setJobOutput:newString];
    }
    else if ( fileDownload == [self errorFileDownload] ) {
        newString = [[self jobError] stringByAppendingString:newString];
        [self setJobError:newString];
    }
}

-(void)fileDownloadDidBegin:(XGFileDownload *)fileDownload {
    numDownloadingFiles++;
}

-(void)fileDownloadDidFinish:(XGFileDownload *)fileDownload {
    if ( --numDownloadingFiles == 0 ) [self setRunning:NO];;
}

-(void)fileDownload:(XGFileDownload *)fileDownload didFailWithError:(NSError *)error {
    if ( --numDownloadingFiles == 0 ) [self setRunning:NO];;
    [[NSAlert alertWithError:error] runModal];
}

Most of these methods simply keep track of how many downloads are still in progress, for the purpose of updating the user interface. The method of most interest is the first: fileDownload:didReceiveData:. This accepts one of the downloaded chunks of data, converts it to a string, and adds it to the string data that has already been downloaded for the stream in question. This string then appears to the user via the magic of Bindings.

Where To Now?

Of course, you can't cover every aspect of a framework like XGridFoundation in an article like this one, but I hope you at least have a flavor of the main classes, how they interact at runtime, and some of the design patterns that recur when you use Xgrid in a Cocoa app.

I have skimped on many issues. For example, how do you upload additional files for a job, or retrieve generated files afterwards? How do you suspend or cancel a running job, or determine its progress? What about connecting to an XGController via Bonjour (the technology formerly known as Rendezvous)? (If you want to try figuring this one out yourself, I highly recommend Mike Beam's original Mac DevCenter articles[1,2].)

Unfortunately, there isn't space for any of this, but having established a basis here, it should not be too difficult for you to find answers by digging around in Apple's Xgrid example projects, which are located in /Developer/Examples/Xgrid.

Drew McCormack works at the Free University in Amsterdam, and develops the Cocoa shareware Trade Strategist.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.