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:
- 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.
- As they note, AppKit is not threadsafe. That means you could have unexpected problems, though I’m not really sure what they’d be.
- 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:
- 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]; - Set the output of your NSTask to a pipe:
NSTask *task = [[NSTask alloc] init]; NSPipe *pipe = [[NSPipe alloc] init]; [task setStandardOutput:pipe]; - Get a file handle from the pipe and start the monitoring process:
NSFileHandle *handle = [pipe fileHandleForReading]; [handle readInBackgroundAndNotify]; - 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