Integrating Xgrid into Cocoa Applications, Part 2
Pages: 1, 2, 3, 4, 5
The initializer stores the controller URL, and creates some component objects,
such as the subTasks dictionary and the subTasksRunning
set. The dictionary is used to store details of the subtasks, and the set is
used to keep track of which of the subtasks are running and thus which are complete.
The taskHasLaunched boolean flag is used to keep track of whether
the task has been launched before. A DistributedTask should only
be launched once; if you need to repeat a calculation, simply create a new DistributedTask.
A number of straightforward accessors follow.
-(void)setDelegate:(id)del {
delegate = del;
}
-(id)delegate {
return delegate;
}
-(unsigned)numberOfSubTasks {
return [subTasks count];
}
-(NSString *)controllerURLString {
return controllerURLString;
}
Note that many attributes of the class do not include setters. You can't change the controller URL after initialization, for example. Making aspects of a class immutable, like this, simplifies writing and using the class.
The method to add subtasks to the task looks like this:
// The working directory, output directory, standard input and output are all
// optional. Use nil if they are not to be used.
#define SubNSNullForNil(var) ( var == nil ? (id)[NSNull null] : (id)var )
-(void)addSubTaskWithIdentifier:(id)identifier
launchPath:(NSString *)launchPath
workingDirectoryPath:(NSString *)workingDirPath
outputDirectoryPath:(NSString *)outputDirPath
standardInputPath:(NSString *)standardInputPath
standardOutputPath:(NSString *)standardOutputPath {
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
identifier, SubTaskIdKey,
launchPath, LaunchPathKey,
SubNSNullForNil(workingDirPath), WorkingDirectoryKey,
SubNSNullForNil(outputDirPath), OutputDirectoryKey,
SubNSNullForNil(standardInputPath), StandardInputKey,
SubNSNullForNil(standardOutputPath), StandardOutputKey,
nil];
[subTasks setObject:dict forKey:identifier];
// Also add to the subTasksRunning set, which will be used to keep track of
// which subTasks have finished when the task is run.
[subTasksRunning addObject:identifier];
}
This method has a lot of arguments. Each argument corresponds directly to
an xgrid command option. As the comment states, you can pass nil
for some of the arguments, and these will then not be passed along to xgrid.
The method itself adds a dictionary describing the subtask to the dictionary
of all subtasks. The identifier of the subtask is used as the key in the subtasks
dictionary. The keys of the dictionary describing the subtask were introduced
above, and correspond directly with the arguments of the method. Note that arguments
that are nil are substituted with an instance of NSNull,
using the macro SubNSNullForNil. This is necessary because you
can't enter nil in a dictionary.
The last line of the method adds the subtask identifier to the subTasksRunning
mutable set. As explained above, this set will be used to determine how many
subtasks are running, and how many have completed.
Now we are getting to the core of the class.
-(void)launch {
// Make sure this is the first launch
if ( taskHasLaunched )
@throw [NSException exceptionWithName:@"MultipleLaunchException"
reason:@"Attempt to launch DistributedTask multiple times."
userInfo:nil];
else
taskHasLaunched = YES;
// Launch subtasks
NSEnumerator *subTaskEn = [subTasks objectEnumerator];
NSMutableDictionary *subTaskDict;
while ( subTaskDict = [subTaskEn nextObject] ) {
// Create thread where xgrid task will be run
[NSThread detachNewThreadSelector:
@selector(runSubTaskAsynchronouslyWithDictionary:)
toTarget:self
withObject:subTaskDict];
}
// Notify delegate of launch and initial progress
if ( delegate &&
[delegate respondsToSelector:
@selector(distributedTaskDidLaunchSubTasks:)] )
[delegate distributedTaskDidLaunchSubTasks:self];
}
The launch method runs the task. It first checks to see if the
task has previously run; if it has, an exception is thrown. I use the new Objective-C
exception handling facilities throughout Photo Industry, so if you haven't given
them a look yet, this is a good opportunity.
Next, we iterate over the subtasks that are in the subTasks dictionary.
For each one, we split off a new thread. The new thread is detached with instructions
to call back to the runSubTaskAsynchronouslyWithDictionary: method
of the DistributedTask object, which is set as the target, and
to pass the subTaskDict dictionary to the method.
After all threads have been launched, a message is sent to the delegate of
the class to indicate this. As usual, this message is only sent if there is
a delegate set, and the delegate responds to distributedTaskDidLaunchSubTasks:.
The runSubTaskAsynchronouslyWithDictionary: method that each
thread calls is something of a monster.
#define ObjectForKeyIsNSNull(dict, key) \
([[dict objectForKey:key] isKindOfClass:[NSNull class]])
-(void)runSubTaskAsynchronouslyWithDictionary:(NSDictionary *)subTaskDict {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
@try {
// Determine the directory where the launch file is
NSString *launchPath =
[subTaskDict objectForKey:LaunchPathKey];
NSString *launchFileName =
[launchPath lastPathComponent];
NSString *launchPathDir =
[launchPath stringByDeletingLastPathComponent];
// Create and setup NSTask for xgrid command
// Run task in the directory of the launch file
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setCurrentDirectoryPath:launchPathDir];
// Create an array with arguments for the NSTask
NSMutableArray *args = [NSMutableArray arrayWithObjects:
@"-h", controllerURLString,
@"-job", @"run",
nil];
if ( ! ObjectForKeyIsNSNull(subTaskDict, WorkingDirectoryKey) ) {
[args addObject:@"-in"];
[args addObject:[subTaskDict objectForKey:WorkingDirectoryKey]];
}
if ( ! ObjectForKeyIsNSNull(subTaskDict, OutputDirectoryKey) ) {
[args addObject:@"-out"];
[args addObject:[subTaskDict objectForKey:OutputDirectoryKey]];
}
if ( ! ObjectForKeyIsNSNull(subTaskDict, StandardInputKey) ) {
[args addObject:@"-si"];
[args addObject:[subTaskDict objectForKey:StandardInputKey]];
}
if ( ! ObjectForKeyIsNSNull(subTaskDict, StandardOutputKey) ) {
[args addObject:@"-so"];
[args addObject:[subTaskDict objectForKey:StandardOutputKey]];
}
[args addObject:launchFileName];
[task setArguments:args];
[task setLaunchPath:@"/usr/bin/xgrid"];
// Launch task, and wait for it to finish
[task launch];
[task waitUntilExit];
int status = [task terminationStatus];
if (status != 0) NSLog(@"xgrid task failed with status code %d", status);
}
@catch (NSException *exception) {
NSLog(@"An %@ exception was raised in runSubTaskAsynchronouslyWithDictionary: %@",
[exception name], [exception reason] );
}
@finally {
[self performSelectorOnMainThread:@selector(subTaskDidFinishWithDictionary:)
withObject:subTaskDict waitUntilDone:NO];
[pool release];
}
}
It begins by initializing an NSAutoreleasePool, and ends by releasing
it. You should do this in any method that is called by a new thread. An autorelease
pool is created for you in the main thread, but not in threads that you create
yourself. If you don't setup an autorelease pool in each new thread, you will
get annoying warning messages in the log, and every call to the autorelease
method will result in a memory leak.
The rest of the method is embedded in a @try-@catch-@finally
block. This is to catch any exceptions that may arise in the worker thread,
and which could cause the program to crash. The exception handling here is not
very advanced, but it is better than nothing. Basically, if something goes wrong,
a log message is printed, and control returned to the main thread as if everything
went to plan. It would be better to inform the user of the problem, but we'll
leave that to version 2.0. Note that a @finally block gets executed
whether an exception is raised or not, so the autorelease pool is always released,
preventing potential memory leaks.
runSubTaskAsynchronouslyWithDictionary: simply extracts the data
passed to it via the dictionary, and adds it to an array, with each entry coupled
to an appropriate xgrid option. For example, the input directory,
which becomes the working directory on the agent, is added to the array by first
inserting the option string @"-in", followed by the path retrieved
from the dictionary. This option is only included if the working directory value
in the dictionary is not an instance of the class NSNull.

