How to gradually retrieve data from NSTask? - cocoa

I am working on a GUI (Cocoa) for a command-line tool to make it more accessible to people. Despite it being a GUI, I would like to display the output to an NSTextView. The problem is that the output is large and the analysis the tool carries out can take hours/days.
Normally, when working with NSTask and NSPipe, the output is displayed only after the task is completely finished (which can take a long time). What I want to do is split the output up and display it gradually (updating every minute for example).
So far I have placed the processing of the data in a separate thread:
[NSThread detachNewThreadSelector:#selector(processData:) toTarget:self withObject:raxmlHandle];
- (void)processData:(id)sender {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *startString = [results string];
NSString *newString = [[NSString alloc] initWithData:[raxmlHandle readDataToEndOfFile] encoding:NSASCIIStringEncoding];
[results setString:[startString stringByAppendingString:newString]];
[startString release];
[newString release];
[pool release];
}
All this is still a bit of voodoo to me and I am not exactly sure how to deal with this challenge.
Do you have any suggestions or recommendations?
Thanks!

You need to use a notification provided by NSFileHandle.
First, add yourself as an observer to the NSFileHandleReadCompletionNotification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(outputReceived:)
name:NSFileHandleReadCompletionNotification
object:nil];
Then, prepare a task, call readInBackgrounAndNotify of the file handle of the pipe, and launch the task.
NSTask*task=[[NSTask alloc] init];
[task setLaunchPath:...];
NSPipe*pipe=[NSPipe pipe];
[task setStandardOutput:pipe];
[task setStandardError:pipe];
// this causes the notification to be fired when the data is available
[[pipe fileHandleForReading] readInBackgroundAndNotify];
[task launch];
Now, to actually receive the data, you need to define a method
-(void)outputReceived:(NSNotification*)notification{
NSFileHandle*fh=[notification object];
// it might be good to check that this file handle is the one you want to read
...
NSData*d=[[aNotification userInfo] objectForKey:#"NSFileHandleNotificationDataItem"];
... do something with data ...
}
You might want to read Notification Programming Topics to understand what is going on.

Related

Using NSPipe with NSTask to simulate command line input

I'm writing a very simple GUI tool for a command line app. It only has 2 buttons. Connect and Quit.
In applicationDidFinishLaunching I run the following
NSPipe *pipe = [[NSPipe alloc] init];
writer = [pipe fileHandleForWriting];
NSTask *runTask = [[[NSTask alloc] init] autorelease];
NSString *exefile = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent:#"vpngui"];
[runTask setLaunchPath: exefile];
NSString *exeDir = [[NSBundle mainBundle] resourcePath];
NSArray *pargs;
pargs = [NSArray arrayWithObjects: exeDir, nil];
[runTask setArguments: pargs];
[runTask setStandardInput:pipe];
[runTask launch];
Then when the Connect button is clicked the following line of code is run
writer writeData:[#"Connect" dataUsingEncoding:NSUTF8StringEncoding]];
and for the Quit button
writer writeData:[#"Quit" dataUsingEncoding:NSUTF8StringEncoding]];
Somehow the command line app never gets the Connect and Quit commands
The data is probably buffered.
Try forcing to flush the file handle by calling the synchronizeFile: method:
synchronizeFile
Causes all in-memory data and attributes of the file represented by the receiver to be written to permanent storage.
I meet the same problem and
closeFile: works for me.
closeFile: to indicate that you have finished writing.
But next time you want to write you'd have to create a new fileHandle instance because the old one has been closed.

RTF to HTML conversion using Cocoa

I am a QT programmer and Cocoa is new for me. How can convert Rich Text Format(RTF text with images and hyper link) to HTML using cocoa under Mac OS X. I have rich text into a char type buffer.
Create an NSAttributedString instance with the RTF data. Walk the attribute ranges of the string (this includes the attachments / pictures) and translate to HTML (append the proper HTML to an NSMutableString) as you go. This lets you convert any attributes you'd like while leaving behind those you don't want.
A helpful NSAttributedString method would be -enumerateAttributesInRange:options:usingBlock:. Inside the block you can determine whether you want to handle or ignore a given attribute. See the Handling Attachments section for information regarding dealing with your images (considered an attachment in RTFD, just another attribute type for a "character" in the attributed string).
You can perform the conversion of RTF to HTML using NSAttributedString and the additions to it provided by Application Kit - read NSAttributedString Application Kit Additions Reference.
The are methods to create an NSAttributedString from RTF, such as initWithRTF:documentAttributes: and initWithURL:documentAttributes:.
To create HTML you can use dataFromRange:documentAttributes:error: specifying appropriate attributes, you'll need to specify at least NSHTMLTextDocumentType.
Depends really on your output requirements… i could convert the string "hello world" to valid html, but it might not be what you were expecting… anyway as an alternative approach to #Joshua's…
The textutil utility can be useful for all sorts of conversions. You can use it from Cocoa via NSTask
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: #"/usr/bin/textutil"];
[task setArguments: #[#"-format", #"rtf", #"-convert", #"html", #"-stdin", #"-stdout"]];
[task setStandardInput:[NSPipe pipe]];
[task setStandardOutput:[NSPipe pipe]];
NSFileHandle *taskInput = [[task standardInput] fileHandleForWriting];
[taskInput writeData:[NSData dataWithBytes:cString length:cStringLength]];
[task launch];
[taskInput closeFile];
Synchronously
NSData *outData = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile];
NSString *outStr = [[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding];
or async
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readCompleted:) name:NSFileHandleReadToEndOfFileCompletionNotification object:[[task standardOutput] fileHandleForReading]];
[[[task standardOutput] fileHandleForReading] readToEndOfFileInBackgroundAndNotify];
- (void)readCompleted:(NSNotification *)notification {
NSData *outData = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *outStr = [[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding];
NSLog(#"Read data: %#", outStr);
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:[notification object]];
}
Please don't just copy and paste this though.. it's just an example, not tested production code.

Get Notification of task progress from NSTask

Any body have idea about getting notification from NSTask while NSTask is executed. I am unzipping a zip file using NSTask and need to show the unzip data progress in a NSProgressBar.
I don't found any idea for doing such task.So that i show the value in progress bar.
Need help for doing this task.
Thanks in advance.
Use NSFileHandleReadCompletionNotification, NSTaskDidTerminateNotification notifications.
task=[[NSTask alloc] init];
[task setLaunchPath:Path];
NSPipe *outputpipe=[[NSPipe alloc]init];
NSPipe *errorpipe=[[NSPipe alloc]init];
NSFileHandle *output,*error;
[task setArguments: arguments];
[task setStandardOutput:outputpipe];
[task setStandardError:errorpipe];
output=[outputpipe fileHandleForReading];
error=[errorpipe fileHandleForReading];
[task launch];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receivedData:) name: NSFileHandleReadCompletionNotification object:output];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receivedError:) name: NSFileHandleReadCompletionNotification object:error];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(TaskCompletion:) name: NSTaskDidTerminateNotification object:task];
//[input writeData:[NSMutableData initWithString:#"test"]];
[output readInBackgroundAndNotify];
[error readInBackgroundAndNotify];
[task waitUntilExit];
[outputpipe release];
[errorpipe release];
[task release];
[pool release];
/* Called when there is some data in the output pipe */
-(void) receivedData:(NSNotification*) rec_not
{
NSData *dataOutput=[[rec_not userInfo] objectForKey:NSFileHandleNotificationDataItem];
[[rec_not object] readInBackgroundAndNotify];
[strfromdata release];
}
/* Called when there is some data in the error pipe */
-(void) receivedError:(NSNotification*) rec_not
{
NSData *dataOutput=[[rec_not userInfo] objectForKey:NSFileHandleNotificationDataItem];
if( !dataOutput)
NSLog(#">>>>>>>>>>>>>>Empty Data");
[[rec_not object] readInBackgroundAndNotify];
}
/* Called when the task is complete */
-(void) TaskCompletion :(NSNotification*) rec_not
{
}
In order to show progress, you need to find out two things:
How many files there are in the archive, or how many bytes they will occupy after unzipping is complete
How many files or bytes you have unzipped so far
You'll find these out by reading the output from the unzip task. Parag Bafna's answer is a start; in receivedData:, you'll need to parse the output to determine what progress has just happened, and then add that progress to your running count of progress so far (e.g., ++_filesUnzippedSoFar).
The first part, finding out the total size of the job, is trickier. You basically need to run unzip before you run unzip: the first, with -l (that's a lowercase L), is to List the contents of the archive; the second is to unzip it. The first one, you read the output to determine how many files/bytes the archive contains; the second one, you read the output to determine the value to advance the progress bar to.
Setting the properties of the progress bar is the easy part; those are literally just doubleValue and maxValue. Working out where you are in the job is the hard part, and is very domain-specific—you need to read unzip's output (twice, in different forms), understand what it's telling you, and translate that into progress information.
There is nothing in NSTask that can help you with that. NSTask's part of this begins and ends at the standardOutput property. It has no knowledge of zip files, archives, contents of archives, or even progress, since none of that applies to most tasks. It's all specific to your task, which means you have to write the code to do it.

NSTask Does Not Terminate

I'm trying to use NSTask to run the UNIX 'apropos' command. Here's my code:
NSTask *apropos = [[NSTask alloc] init];
NSPipe *pipe = [[NSPipe alloc] init];
[apropos setLaunchPath:#"/usr/bin/apropos"];
[apropos setArguments:[NSArray arrayWithObjects:#"filename", #"match", nil]];
[apropos setStandardOutput:pipe];
[apropos launch];
[apropos waitUntilExit];
The problem is that this never returns. I also tried using Apple's example code (TaskWrapper) and it returns the output (in three segments) but it never calls the processFinished handler.
Furthermore, the appendOutput: handler receives duplicates. So, for example, if apropos returns this:
1
2
3
4
5
I might receive something like this:
1
2
3
1
2
3
4
5
(grouped into 3 append messages).
I note that Apropos displays the output in a format where it's possible to scroll up and down in the command line instead of just directly outputting the data straight to standard output; how do I read this reliably through NSTask and NSPipe?
I’ve just tested this program and it works fine: the program terminates and /tmp/apropos.txt contains the output of apropos.
#import <Foundation/Foundation.h>
int main()
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSTask *apropos = [[[NSTask alloc] init] autorelease];
NSPipe *pipe = [[[NSPipe alloc] init] autorelease];
NSFileHandle *readHandle = [pipe fileHandleForReading];
[apropos setLaunchPath:#"/usr/bin/apropos"];
[apropos setArguments:[NSArray arrayWithObjects:#"filename", #"match", nil]];
[apropos setStandardOutput:pipe];
[apropos launch];
[apropos waitUntilExit];
NSString *output = [[[NSString alloc]
initWithData:[readHandle readDataToEndOfFile]
encoding:NSUTF8StringEncoding] autorelease];
[output writeToFile:#"/tmp/apropos.txt" atomically:YES
encoding:NSUTF8StringEncoding error:NULL];
[pool drain];
return 0;
}
Are you by any chance using NSLog() to inspect the output? If so, you might need to set a pipe for stdin as explained in this answer of mine to an NSTask related question. It seems that NSLog() sending data to stderr affects NSTask.
With your original code, I would imagine it's because you're not reading the output of the command. The pipes only have a limited buffer size, and if you don't read the output of the task, it can end up hung waiting for the buffer to empty out. I don't know anything about the sample code you tried so I can't help there. As for the last question, apropos only uses the pager when it's connected to a terminal. You're not emulating a terminal, so you don't have to worry. You can prove this by running apropos whatever | cat in the terminal and verifying that the pager is not invoked.

Cocoa Memory Management NSArray with Objects

I'm having troubles releasing objects.. To explain it better I have included my code below.
NSTask *task = [NSTask new];
NSTask *grep = [NSTask new];
NSPipe *pipe = [NSPipe new];
[task setStandardError: pipe];
[grep setStandardInput: pipe];
[pipe release];
pipe = [NSPipe new];
[grep setStandardOutput: pipe];
[task launch];
[grep launch];
NSString *string = [[[[[[NSString alloc] initWithData: [[[grep standardOutput] fileHandleForReading] readDataToEndOfFile] encoding: NSASCIIStringEncoding] autorelease] componentsSeparatedByString: #" "] objectAtIndex: 3] substringToIndex: 8];
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict setObject: string forKey: #"myKey"];
[records addObject: dict];
[dict release];
[task release];
[grep release];
[pipe release];
How would I release the string and are there any other leaks? Also, if I remove everything from the array records with removeAllObjects, is everything released okay then too? The array should never be released and be available at all time, I'm just worrying about its objects.
Edit: The only leak pointed out had to do with the NSPipe and should be fixed in the code.
Thanks for any help!
Memory management in Objective-C has one fundamental rule:
You take ownership of an object if you create it using a method whose name begins with “alloc” or “new” or contains “copy” (for example, alloc, newObject, or mutableCopy), or if you send it a retain message. You are responsible for relinquishing ownership of objects you own using release or autorelease. Any other time you receive an object, you must not release it.
Thus every call to new in your code sample should be balanced with a call to release or autorelease. The NSArray, along with most other objects in the code, isn't created with either, so it doesn't need to be released. The [NSString alloc] is autoreleased, so it's taken care of. Collections manage their own items, retaining and releasing them as necessary: when an item is inserted, it's retained; when it's removed, it's released. Dictionary keys are copied rather than retained.
Where you've got an unbalanced new (and hence leak) is the first NSPipe you created. Release it before creating the pipe for grep's standard output. Perhaps you simply left it out of the sample, but you're also not setting any arguments for the grep task.
substringToIndex: returns an autoreleased string, so there's no need to release it.
The only memory leak I see is where you set up your 'pipe' var a second time (for the task's standard output) without first releasing its current value (the NSPipe instance used for standard error & input).
Mutable collections like NSMutableArray will retain the objects they contain (as do all mutable/nonmutable collections) then release them when they're removed (or when the collection itself is deallocated).

Resources