Get Notification of task progress from NSTask - macos

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.

Related

Getting a crash in NSTask

I've had a report from the field of a crash at -launch on NSTask.
The code in question is:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/zsh"];
if(ignore)
{
[task setArguments:#[scriptPath, recordingFolder, Argument]];
}
else
{
[task setArguments:#[scriptPath, recordingFolder]];
}
NSPipe *outPipe = [NSPipe pipe];
[task setStandardOutput:outPipe];
NSPipe *errorPipe = [NSPipe pipe];
[task setStandardError:errorPipe];
[task launch];
The scriptPath is a script that is included in the app bundle.
The crash says:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Failed to set posix_spawn_file_actions for fd -1 at index 0 with errno 9'
What could be the cause of this? What file descriptor do the posix_spawn_file_actions refer to? Does it mean that the executable script is wrong or that the outPipe or errPipe are not well formed?
I believe it is referring to the posix_spawn function:
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/posix_spawn.2.html
And errno 9 is EBADF (bad file number).
I've got a similar error , after I use below command, it's OK, you can try it.
NSFileHandle *file=[outPipe fileHandleForReading];
[task launch];
....//read file.
//this is the most important.
[file closeFile];
If you are calling this code many different times sequentially, you are running out of file descriptors -- you need to close the pipes after you are done. The correct way to do it is to deallocate NSTask and it will close the file channels. Put the NSTask-related code in an autoreleasepool statement:
#autoreleasepool {
NSTask* task = [NSTask new];
...
[task launch];
...
[task waitUntilDone];
}
For the issue in the question, I suggest that we should put the code of CreateProcess() in an #autoreleasepool block as Apple's doc shows that we should not send -[closeFile] to fileHandleForReading explicitly.
https://developer.apple.com/reference/foundation/nspipe/1414352-filehandleforreading?language=objc
Declaration
#property(readonly, retain) NSFileHandle *fileHandleForReading;
Discussion
The descriptor represented by this object is deleted, and the object
itself is automatically deallocated when the receiver is deallocated.
You use the returned file handle to read from the pipe using
NSFileHandle's read methods—availableData, readDataToEndOfFile, and
readDataOfLength:.
You don’t need to send closeFile to this object or explicitly release
the object after you have finished using it.

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.

How to gradually retrieve data from NSTask?

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.

What is the best way to redirect stdout to NSTextView in Cocoa?

Hi: I want to redirect stdout to a NSTextView. Could this also work with outputs of subprocesses? What might be the best way to achieve this?
EDIT:
According to Peter Hosey answer I implemented the following. But I do not get a notification. What am I doing wrong?
NSPipe *pipe = [NSPipe pipe];
NSFileHandle *pipeHandle = [pipe fileHandleForWriting];
dup2(STDOUT_FILENO, [pipeHandle fileDescriptor]);
NSFileHandle *fileHandle = [[NSFileHandle alloc] initWithFileDescriptor:pipeHandle];
[fileHandle acceptConnectionInBackgroundAndNotify];
NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
[dnc addObserver:self selector:#selector(handleNotification:) name:NSFileHandleConnectionAcceptedNotification object:fileHandle];
I was looking to do the same thing and came across this post. I was able to figure it out after reading this and Apple's documentation. I'm including my solution here. "pipe" and "pipeReadHandle" are declared in the interface. In the init method I included the following code:
pipe = [NSPipe pipe] ;
pipeReadHandle = [pipe fileHandleForReading] ;
dup2([[pipe fileHandleForWriting] fileDescriptor], fileno(stdout)) ;
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(handleNotification:) name: NSFileHandleReadCompletionNotification object: pipeReadHandle] ;
[pipeReadHandle readInBackgroundAndNotify] ;
The handleNotification: method is
[pipeReadHandle readInBackgroundAndNotify] ;
NSString *str = [[NSString alloc] initWithData: [[notification userInfo] objectForKey: NSFileHandleNotificationDataItem] encoding: NSASCIIStringEncoding] ;
// Do whatever you want with str
I want to redirect stdout to a NSTextView.
Your own stdout?
Could this also work with outputs of subprocesses?
Sure.
What might be the best way to achieve this?
File descriptors are your friend here.
Create a pipe (using either NSPipe or pipe(2)) and dup2 its write end onto STDOUT_FILENO. When invoking subprocesses, don't set their stdout; they'll inherit your stdout, which is your pipe. (You may want to close the read end in the subprocess, though. I'm not sure whether this will be necessary; try it and find out. If it does turn out to be, you'll need to use fork and exec, and close the read end in between.)
Read from the read end of the pipe in the background asynchronously, using either kevent or NSFileHandle. When new data comes in, interpret it using some encoding and append it to the contents of the text view.
If the text view is in a scroll view, you should check the scroll position before appending to it. If it was at the end, you'll probably want to jump it back to the end after appending.
Have a look at PseudoTTY.app!
For iOS use stderr instead of stdout.
NSPipe *pipe = [NSPipe pipe];
pipeHandle = [pipe fileHandleForReading];
dup2([[pipe fileHandleForWriting] fileDescriptor], fileno(stderr));
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(handleNotification:) name: NSFileHandleReadCompletionNotification object: pipeHandle] ;
[pipeHandle readInBackgroundAndNotify] ;
-(void)handleNotification:(NSNotification *)notification{
[pipeHandle readInBackgroundAndNotify] ;
NSString *str = [[NSString alloc] initWithData: [[notification userInfo] objectForKey: NSFileHandleNotificationDataItem] encoding: NSASCIIStringEncoding] ;
}

Saving System Profiler info in a .spx file using NSTask

In Cocoa, I am trying to implement a button, which when the user clicks on will capture the System profiler report and paste it on the Desktop.
Code
NSTask *taskDebug;
NSPipe *pipeDebug;
taskDebug = [[NSTask alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:selfselector:#selector(taskFinished:) name:NSTaskDidTerminateNotification object:taskDebug];
[profilerButton setTitle:#"Please Wait"];
[profilerButton setEnabled:NO];
[taskDebug setLaunchPath: #"/usr/sbin/system_profiler"];
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",#">", #"
~/Desktop/Profiler.spx",nil];
[taskDebug setArguments:args];
[taskDebug launch];
But this does not save the file to the Desktop. Having
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",nil]
works and it drops the whole sys profiler output in the Console Window.
Any tips on why this does not work or how to better implement this ? I am trying to refrain from using a shell script or APpleScript to get the system profiler. If nothing work's that would be my final option.
Thanks in advance.
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"-detailLevel",#"full",#">", #"~/Desktop/Profiler.spx",nil];
That won't work because you aren't going through the shell, and > is a shell operator. (Also, ~ isn't special except when you expand it using stringByExpandingTildeInPath.)
Create an NSFileHandle for writing to that Profiler.spx file, making sure to use the full absolute path, not the tilde-abbreviated path. Then, set that NSFileHandle as the task's standard output. This is essentially what the shell does when you use a > operator in it.
This got it done ( thanks to Peter and Costique)
[taskDebug setLaunchPath: #"/usr/sbin/system_profiler"];
NSArray *args = [NSArray arrayWithObjects:#"-xml",#"- detailLevel",#"full",nil];
[taskDebug setArguments:args];
[[NSFileManager defaultManager] createFileAtPath: [pathToFile stringByExpandingTildeInPath] contents: nil attributes: nil];
outFile = [ NSFileHandle fileHandleForWritingAtPath:[pathToFile stringByExpandingTildeInPath]];
[taskDebug setStandardOutput:outFile];
[taskDebug launch];
Create an NSPipe, send [taskDebug setStandardOutput: myPipe] and read from the pipe's file handle.

Resources