macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Integrating Xgrid into Cocoa Applications, Part 2
Pages: 1, 2, 3, 4, 5

Controlling the Industry

The last part of the puzzle is the Cocoa controller class, PIController, which prepares the Xgrid job, and takes care of the User Interface (UI). We won't deal with the UI here; the source code is there for all to see. Instead, we will concentrate only on those parts of the controller that deal with preparing jobs for Xgrid.



The awakeFromNib method of PIController reads the filters.plist property list file to initialize the filters available in Photo Industry.


-(void)awakeFromNib {
    ...
    // Initialize available filters from plist file
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];
    NSString *plistPath = [bundle pathForResource:@"filters" ofType:@"plist"];
    NSData *data = [NSData dataWithContentsOfFile:plistPath];
    NSString *errorString;
    NSArray *filtersArray = [NSPropertyListSerialization propertyListFromData:data 
        mutabilityOption:NSPropertyListMutableContainers 
        format:NULL 
        errorDescription:&errorString];
    NSAssert( nil != filtersArray, @"Could not read property list of filters." );
    [self setFilters:filtersArray];
}

The main NSBundle is used to locate the file, and the data is then read into an instance of NSData. This data is turned into an array of filter information by the NSPropertyListSerialization class. There are easier ways to do this, like simply calling the NSArray method arrayWithContentsOfFile:, but we have taken the long route because we want our array to be populated with mutable objects. The option NSPropertyListMutableContainers achieves this objective. The objects need to be mutable, because they will be used to store whether a filter is on or off, and this can be changed by the user.

You may be wondering what sort of objects make up filtersArray. They are simply NSMutableDictionarie's, as you can see by taking a look in the filters.plist file.


<?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">
<array>
    <dict>
        <key>filterId</key>
        <string>thumbnail</string>
        <key>filterName</key>
        <string>Thumbnail</string>
        <key>isOn</key>
        <false/>
    </dict>
    <dict>
        <key>filterId</key>
        <string>blur</string>
        <key>filterName</key>
        <string>Blur</string>
        <key>isOn</key>
        <false/>
    </dict>

As you can see, each dictionary in the array holds key-value pairs for the filter's identity, which is used in Photo Industry and the Python agent script to refer to the filter -- the filter's name, which is what the filter is called in the UI -- and whether it is on or off. All are initially turned off, but the user can turn on filters in the UI, and this requires that the dictionary entry be able to change (i.e., be mutable).

Most of the PIController code that relates to Xgrid can be found in the method applyFilters:toFilesWithPaths:forOutputDirectoryPath:. If you write your own Xgrid-enabled Cocoa app, you will likely need a very similar method, so let's take a good look at how it works.

We will skip anything that is related to the UI, rather than Xgrid. Here is how the method begins.


-(void)applyFilters:(NSArray *)filter toFilesWithPaths:(NSArray *)paths
    forOutputDirectoryPath:(NSString *)eventualOutputDirPath {

    ...
        
    // Create distributed task
    NSFileManager *fm = [NSFileManager defaultManager];
    DistributedTask *task = 
        [[[DistributedTask alloc] initWithControllerURLString:@"localhost"] autorelease];
    
    // Store the distributed task. Register for delegate messages.
    [self setDistributedTask:task];
    [task setDelegate:self];

Here we are introduced to the class NSFileManager. You had better get used to it, because this guy is going to be your partner for much of the rest of this article. NSFileManager takes care of the stuff that commands like mv, cp, rm, and ln do in a shell. Anytime you need to move, copy, remove, or link a file, you know who you have to see.

At this point we also create our distributed task, which is our interface to Xgrid. The PIController is made delegate of the task, and the DistributedTask is initialized to make use of a controller on the local machine, localhost. No attempt is made to check whether there is actually a controller on the local host, and it is not possible to run Photo Industry using a controller on another computer.

This simplistic approach could easily be improved upon: Controllers advertise themselves with Rendezvous, so you can go looking for them, and when you find them, you can query them about things like how many nodes they have available, using the xgrid command-line tool (see the -node option in the xgrid man page). Such an approach would lead to a much more flexible piece of software, but it is too advanced for this introduction. You can read a good introduction to using Rendezvous in Cocoa by Mike Beam here.

PIController then sets up some directories for the task.


    // Set the output directory path.
    [self setOutputDirectoryPath:eventualOutputDirPath];

    // Setup a temporary directory.
    // Also create a directory where the output will end up.
    NSString *uniqueString = [[NSProcessInfo processInfo] globallyUniqueString];
    NSString *dirName = 
        [NSString stringWithFormat:@"photoindustry_%@", uniqueString];
    [self setTaskTempDirectoryPath:
        [NSTemporaryDirectory() stringByAppendingPathComponent:dirName]];
    NSString *taskOutputDirPath = 
        [taskTempDirPath stringByAppendingPathComponent:@"output"];
    
    [fm createDirectoryAtPath:taskTempDirPath attributes:nil];
    [fm createDirectoryAtPath:taskOutputDirPath attributes:nil];

It stores the output directory that the user has requested (i.e. eventualOutputDirPath). This is not the output directory used by DistributedTask, it is the place where all photos must eventually end up. The DistributedTask will put its output in a temporary directory, which is created next. An NSProcessInfo object is used to generate a unique string, which is then used to come up with a name for the tasks temporary directory, reducing the likelihood that any conflict will occur. A temporary directory is created for all files used by the DistributedTask. This is a subdirectory of the directory returned by the Cocoa function NSTemporaryDirectory. In the task's directory, another subdirectory is created exclusively for output from the DistributedTask. The NSFileManager is used to do the directory creation.

Next, a colon-separated list of filters is created, the same one that our Python script received on standard input.


    // Create a standard input file for all subtasks
    // This is just a colon-separated list of the filter ids of the filters
    // that need to be applied
    NSMutableArray *filterStringArray = [NSMutableArray arrayWithCapacity:10];
    NSEnumerator *en = [filters objectEnumerator];
    NSDictionary *filterDict;
    while ( filterDict = [en nextObject] ) {
        BOOL isOn = [[filterDict objectForKey:@"isOn"] boolValue];
        if ( isOn ) 
            [filterStringArray addObject:[filterDict objectForKey:@"filterId"]];
    }
    NSString *stdInString = [filterStringArray componentsJoinedByString:@":"];
    NSString *siPath = 
        [taskTempDirPath stringByAppendingPathComponent:@"standardinput"];
    [stdInString writeToFile:siPath atomically:NO];

We simply iterate over all the filter dictionaries in the filters array, checking if they are on or off. If on, they are added to our list. Lastly, this list is written to a file in the task's directory. Later this file will be set as the standard input of the subtasks in our DistributedTask.

Now the subtasks must be setup.


    // Create an input directory for each subtask in the temporary directory.
    // Copy photos into the input directories of subtasks.
    // Distribute photos as evenly as possible amongst subtasks. If the 
    // number of photos doesn't exactly divide by the number of subtasks, some
    // subtasks are required to process one extra photo. 
    unsigned baseNumPhotosPerSubTask = [paths count] / NumDistributedSubTasks;
    unsigned numSubTasksWithOneExtra = [paths count] % NumDistributedSubTasks;
    unsigned subTaskIndex, photoIndex = 0;
    for ( subTaskIndex = 0; subTaskIndex < NumDistributedSubTasks; 
          subTaskIndex++ ) {
        NSString *subTaskIndexString = 
            [NSString stringWithFormat:@"%d", subTaskIndex];
        NSString *inputDirPath = 
            [taskTempDirPath stringByAppendingPathComponent:subTaskIndexString];
        [fm createDirectoryAtPath:inputDirPath attributes:nil];
        
        // Copy photos to the input directory for the subtask
        unsigned numPhotosThisSubTask = baseNumPhotosPerSubTask;
        if ( subTaskIndex < numSubTasksWithOneExtra ) ++numPhotosThisSubTask;
        if ( numPhotosThisSubTask == 0 ) continue; 
            // Don't start subtask for no photos
        unsigned subTaskPhotoIndex;
        for ( subTaskPhotoIndex = 0; subTaskPhotoIndex < numPhotosThisSubTask; 
              subTaskPhotoIndex++ ) {
            NSString *photoPath = [paths objectAtIndex:photoIndex];
            [fm copyPath:photoPath toDirectoryAtPath:inputDirPath];
            photoIndex++;
        }

Some arithmetic is performed to determine how many photos each subtask should process. The algorithm simply tries to spread the number of photos as evenly as possible over the subtasks. If the number of photos does not divide exactly by the number of subtasks, some tasks are required to take one extra photo. The number of subtasks is simply a constant, NumDistributedSubTasks, which is elsewhere set to 4.

A loop over subtasks begins, and an input directory is created for each subtask. The subtask's photos, the paths to which are passed to the method, are then copied to the input directory. Creating a link would be faster, but I found some unnerving behavior whenever a linked file is deleted: the Finder seems to think that all links to a file are deleted when any one of them is. This seems to be an error in Finder, not in the filesystem itself, because the linked file does continue to exist. Nonetheless, I thought it was safer to copy the original files so that should anything go wrong, they would not be lost.

You will not find the method copyPath:toDirectoryAtPath:, which belongs to NSFileManager, in the Cocoa documentation. That's because it belongs to a category that I have created in Photo Industry. This is what it looks like.


@interface NSFileManager (PIControllerExtensions)
-(void)copyPath:(NSString *)path toDirectoryAtPath:(NSString *)inputDir;
@end


@implementation NSFileManager (PIControllerExtensions)

-(void)copyPath:(NSString *)path toDirectoryAtPath:(NSString *)dirPath {
    NSString *filePathInDir = 
        [dirPath stringByAppendingPathComponent:[path lastPathComponent]];    
    [[NSFileManager defaultManager] copyPath:path toPath:filePathInDir handler:nil];
}

@end

This method is a convenience, because we regularly need to copy files to directories, and it is a bit annoying to have to keep using the stringByAppendingPathComponent: method to first setup the new file path, when the file name does not need to change.

Pages: 1, 2, 3, 4, 5

Next Pagearrow