Better way to read from an NSTask

rharder

Do not read this sign.
An earlier thread had information on using NSTimer-s to read from an NSPipe attached to an NSTask. This required regular polling and reading from the NSPipe which seems like kind of a silly way to do it. Apparently there's a better way, and boy am I glad it's there. I'll share it with you.

Let's assume you have some controller class that's going to start up the NSTask and do something with the NSPipe you read. Maybe you're just going to display the results in an NSTextView that you created in Project Builder.

In the controller's .h file, let's declare some variables:
Code:
NSTask       *_task;
NSFileHandle *_fileHandle;
Of course you would have put the _task in there anyway, right, but we want to have access to the _fileHandle that will become very important soon.

As always we launch the NSTask:
Code:
NSPipe *pipe = [NSPipe pipe];

_fileHandle = [pipe fileHandleForReading];
[_fileHandle readInBackgroundAndNotify];

_task = [[NSTask alloc] init];
[_task setLaunchPath:@"/bin/ls"];
[_task setStandardOutput: pipe];
[_task setStandardError: pipe];
// arguments if you want 'em
[_task launch];
Now you'll be notified when the file handle (and thus the pipe attached to the task) has data to read. Oh wait, no you won't. We have to register with the NSNotificationCenter.

Make up an init method like this:
Code:
-(id)init
{
    [super init];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector( readPipe: )
        name:NSFileHandleReadCompletionNotification 
        object:nil];
    return self;
}
Of course you may need to add your own stuff too. Don't forget to unregister when you dealloc:
Code:
-(void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
So what does our readPipe method look like? This:
Code:
-(void)readPipe: (NSNotification *)notification
{
    NSData *data;
    NSString *text;

    if( [notification object] != _fileHandle )
        return;

    data = [[notification userInfo] 
            objectForKey:NSFileHandleNotificationDataItem];
    text = [[NSString alloc] initWithData:data 
            encoding:NSASCIIStringEncoding];

    // Do something with your text
    // ...

    [text release];
    if( _task )
        [_fileHandle readInBackgroundAndNotify];
}
Note that at the end we have to tell the file handle again that we want to be notified of changes, but we check to make sure that the _task isn't nil (assuming we did that somewhere).

Now isn't active notification much better than polling?

Let me know, y'all, if I have any errors.

-Rob
 
Thank you! I have been looking for <i>exactly</i> this! I've been having to wait until the task has finished before seeing the results. This is much much better!

Now onto my next mountain: opening a png file that's being written to and displaying it on the fly. Sounds like fun :D
 
That would be a trick. I wonder if you could 'tail' a binary file.

Oh, and one of the reasons that polling or waiting until the end of an NSTask is a bad idea is that apparently there's only an 8K or so buffer in the pipe somewhere, and you could lose data otherwise. I think this technique should get around that. Maybe.

Good luck.

-Rob
 
Originally posted by rharder
That would be a trick. I wonder if you could 'tail' a binary file.

Oh, and one of the reasons that polling or waiting until the end of an NSTask is a bad idea is that apparently there's only an 8K or so buffer in the pipe somewhere, and you could lose data otherwise. I think this technique should get around that. Maybe.

Good luck.

-Rob

There is 8KB buffer in pipe kernel implementation. However, that buffer does not lose data; the writing application blocks on write when the buffer is full. So, unless you are reading from the pipe being written to, the writing application will not finish if it has more than 8K to write.
 
Oh, good. So even though it may be less efficient, you could still just use NSTimers (if you really really wanted to) to read data instead of the "active" method mentioned above.

-Rob
 
Rob, I found a limitation in this...sorta kinda. I was wondering if you could help me out.

This works great for input/output (whichever you want to call it) that's not too long, but if the data from the pipe takes longer to display than it takes for the task to finish executing, the data stops displaying. I'm guessing this is from the if(task).... line. I know this is the reason the text gets cut in the display (I'm using an NSTextView to display the results) because sometimes the task won't end (or my app isn't getting that it ended), and all the data gets displayed. (BTW, if anyone out there is having a problem with tasks not finishing, I found a strange workaround. Not in the code, but in the running app. If you click on one of the menus, which won't open, then click on the desktop, then click back on your window, the app will get that the task has finished...for whatever reason. This is most likely a bug in my code, but I'd thought I'd share in case someone else was seeing this).

Anyway, I was wondering if you could help me think of a way to get the last bit of data from the pipe displayed. I tried adding a [_fileHandle readInBackgroundAndNotify] to the task finished part (BTW, I have two observers added in init - seems to work fine, but I'm new to Cocoa, and so I'm asking if that's alright? One for catching the data from the pipe, and one for catching when the task quits). That didn't work. I tried making an else after the if (task) line, but that didn't really work either...I had set up an NSTimer to fire a few times and then had it do [_fileHandle readInBackgroundAndNotify], but it didn't get anything else.

Any thoughts to what may be happening?
 
So, when there's a lot of output, some of the last bit of data doesn't get read?

And you tried reading after the Task Finished notification too?

Weird.

I had something weird like that when I 'tail'ed the system log, but since that task never finished, I never quite had the problem you describe.

I did discover that I had to manually scroll the NSTextView a few times to "reset" the viewing.

I wonder if some of this comes from threading issues between the task and the main event-dispatching thread.

I haven't worried about it too much, figuring it might just be little bugs that will get worked out or explained in a release note some time.

-Rob
 
To get all of the output

change the If statement to look at the data structure instead of the program.


ie.


If( data != 0 )

[_fileHandle readInBackgroundAndNotify];



This will allow the remainder of the pipe to be read after the unix program finishes executing.

 
Won't that cause you to register with the file handle after it's done? When you get the last piece of data and the NSTask is finished, you'd be registering one last time with the file handle.

Oh, but if you use your idea and properly dispose of the file handle when the NSTask is finished (as you ought to anyway) then that registration problem goes away. Maybe.

To be safe, you should probably explicitly remove yourself from the notification center for the file handle notification when the NSTask is finished, not just when you dealloc.

-Rob
 
I did release the handle afterwards and remove myself from the notification center. I should have added this as well.


I have not had any problems since I modified my wrapper in this manner. Of course, this does not mean that they won't pop up in the future.

 
Works great except I want to display output in a uitextview but it doesn't add to my existing uitextview it like overlaps it making it unreadable can u help?
 
Back
Top