banner

Monitoring an NSTask subprocess

Posted: September 8th, 2005 | Author: amake | Filed under: Software | No Comments »

Work on the Cocoa port of iPodBackup has slowed a bit lately since now I’m back at school. I did finally implement something nifty the other day, though—the progress indicator tells you what rsync is doing as it’s running.

I had some trouble getting this to work at first. I found some tutorials on monitoring NSTasks, such as this one at CocoaDevCenter. But there are some big problems with the approach they use:

  1. Polling is inefficient. Rather than asking “Are we there yet?” over and over again, just wait until your parents tell you “We’re there.” With polling I found the app taking way too much CPU time.
  2. As they note, AppKit is not threadsafe. That means you could have unexpected problems, though I’m not really sure what they’d be.
  3. There were strange drawing artifacts in the NSTextField I updated with each poll. This probably could have been fixed by slowing the polling down, but then you risk missing some output.

To be a bit clearer, the task at hand is the following: Capture output from a background process and display it onscreen as the process is running. Doing this can be accomplished efficiently and elegantly by using notifications. Here’s how:

  1. Register your application controller or whatever to receive NSFileHandle’s NSFileHandleReadCompletionNotification:
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self
    	   selector:@selector(myMethod:)
    	       name:@"NSFileHandleReadCompletionNotification"
    	     object:nil];

  2. Set the output of your NSTask to a pipe:
    NSTask *task = [[NSTask alloc] init];
    NSPipe *pipe = [[NSPipe alloc] init];
    [task setStandardOutput:pipe];

  3. Get a file handle from the pipe and start the monitoring process:
    NSFileHandle *handle = [pipe fileHandleForReading];
    [handle readInBackgroundAndNotify];

  4. Begin the task as usual:
    [task launch];

Now when handle gets output from task, a notification will be sent to myMethod:. Attached to the notification will be an NSDictionary containing the output string stored at the key NSFileHandleNotificationDataItem.

The only problem at this point is that readInBackgroundAndNotify is not a repeated thing; it only notifies you once, so in myMethod: you will need to call it again if you want more notifications.

- (void)myMethod:(NSNotification *)note
{
	// Get output string
	NSData *data = [[note userInfo]
                objectForKey:@"NSFileHandleNotificationDataItem"];
	NSString *string = [[NSString alloc] initWithData:data
                                                 encoding:NSUTF8StringEncoding];
	// Put string in a text field
	[myTextField setStringValue:string];
	// Ask for another notification
	if (condition) [[note object] readInBackgroundAndNotify];

	[string release];
}

Additionally, sometimes the task can output so quickly that string is several lines long. If you’re expecting only one line, you can do:

NSString *oneLineString = [[string componentsSeparatedByString:@"\n"]
        objectAtIndex:0];

Update Sun Oct 2 11:16:25 CDT 2005: An additional problem with this approach is that often the NSTaskDidTerminateNotification is sent before the file handle is finished reading data output by the NSTask. I found a great solution to this issue at Cocoabuilder.



Leave a Reply