NSTask not returning [duplicate] - objective-c

This question already has answers here:
NSTask NSPipe - objective c command line help
(2 answers)
Closed 2 years ago.
I use a NSTask as follows:
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/bash"];
NSMutableString *script = ... // actual script here, works fine
NSArray *args = [NSArray arrayWithObjects:#"-l",
#"-c",
script,
nil];
[task setArguments: args];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
[task launch];
NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile];
[task waitUntilExit];
[task release];
Works fine so far, only thing is, that somehow, the method isn't called anymore after first reaching this point. If I don't launch the task, everything's ok, so it would seem, that the task somehow blocks further execution.
Has anybody an idea, why that would be? Any hints appreciated!

Get more control on the task by running a loop where you can read the data, check if it's timed out , etc.:
while ([task isRunning])
{
NSData* data = [[pipe fileHandleForReading] availableData];
if ([data length] > 0)
{
//process the data
}
// Run current runloop in default mode; check the timeout; interrupt the task if needed
}

Related

Cannot read stdout from an NSTask in release mode

I'm reading the stdout of an NSTask like so:
NSPipe *outputpipe = [[NSPipe alloc] init];
NSFileHandle *output = [outputpipe fileHandleForReading];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receivedOutputFromAlgo:) name:NSFileHandleReadCompletionNotification object:output];
NSTask* task = [[NSTask alloc] init];
[task setLaunchPath:[NSString stringWithFormat:#"%#algo/Algo", kVinylRoot]];
[task setArguments:[NSArray arrayWithObject:plist]];
[task setStandardOutput:outputpipe];
[task setCurrentDirectoryPath:[NSString stringWithFormat:#"%#algo", kVinylRoot]];
[task launch];
[output readInBackgroundAndNotify];
[task waitUntilExit];
...
-(void)receivedOutputFromAlgo:(NSNotification*)notification {
[[notification object] readInBackgroundAndNotify];
NSData* dataOutput = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString* dataString = [[NSString alloc] initWithData:dataOutput encoding:NSStringEncodingConversionAllowLossy];
//Do stuff
}
This works just fine if I run this from Xcode. I see the stdout of my task in the console, and receivedOutputFromAlgo is hit multiple times. However, if I archive the app and run it by double clicking the .app package or run it from terminal it doesn't work. I can still see the task stdout coming through in console.app or in the terminal if I run it from there.
Is it not considered stdout at this point? Why would this not work?
Edit: I just tried turning off optimization in release and that didn't work either.
you need handle the setStandardError, like this:
[task setStandardError:outputpipe];

How do I have an NSTextField constantly update its value based on the output of a command line argument?

I am trying to create a small rsync program in Cbjective-C. It presently accesses the terminal command line through an NSTask and reads the command line's output to a string that is displayed in a NSTextField; however, when I use this small program on a very large file (around 8 gb) it does not display the output until after the RSYNC is complete. I want the NSTextField to continually update while the process is running. I have the following code and am looking for ideas!:
-(IBAction)sync:(id)sender
{
NSString *sourcePath = self.source.stringValue;
NSString *destinationPath = self.destination.stringValue;
NSLog(#"The source is %#. The destination is %#.", sourcePath, destinationPath);
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/rsync"];
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"-rptWav", #"--progress", sourcePath, destinationPath, nil];
[task setArguments: arguments];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
// [task setStandardInput:[NSPipe pipe]];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
while ([task isRunning])
{
NSString *readString;
readString = [[NSString alloc] initWithData: data encoding:NSUTF8StringEncoding];
textView.string = readString;
NSLog(#"grep returned:\n%#", readString);
}
}
OK, the issue is with the way in which you are reading the data from the pipe. You are using:
NSData *data = [file readDataToEndOfFile];
Which will read everything written by the child process in one go, up until the pipe closes (when the child process has terminated).
What you need to do is read a character at-at-time and reconstruct the output line. You also want to use non-blocking mode, so that your main UI thread isn't interrupted when there is no data to read (better still, this should be done in a background thread so that the main UI thread remains completely uninterrupted).

NSTask with multiple pipe input

I'm trying to use pipes to handle a command that requires multiple inputs, but not quite sure how to do it. Here's a snippet of what I'm trying to do. I know how to handle the first input, but I'm at lost as to piping in the second input -newstdinpass
NSTask *task = [[NSTask alloc] init];
NSPipe *pipe = [NSPipe pipe];
[task setLaunchPath: #"/bin/sh"];
[task setArguments: [NSArray arrayWithObjects: #"-c", #"/usr/bin/hdiutil chpass -oldstdinpass -newstdinpass /path/to/dmg", nil]];
[task setStandardInput:pipe];
[task launch];
[[pipe fileHandleForWriting] writeData:[#"thepassword" dataUsingEncoding:NSUTF8StringEncoding]];
[[pipe fileHandleForWriting] closeFile];
[task waitUntilExit];
[task release];
So I know using hdiutil in this manner is a bit of a hack, but in terms of pipes, am I going about it the correct way?
Thanks.
UPDATE: In case others are wondering about this, a quick solution to my problem is to pass in a null-terminated string as Ken Thomases had pointed out below. Use [[NSString stringWithFormat:#"oldpass\0newpass\0"] dataUsingEncoding:NSUTF8StringEncoding] into the pipe. Now, still need to learn how to bridge multiple NSTasks with pipes...
You can create multiple NSTasks and a bunch of NSPipes and hook them together, or you can use the sh -c trick to feed a shell a command, and let it parse it and set up all the IPC.
Example :
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"-c",
#"cat /usr/share/dict/words | grep -i ham | rev | tail -5", nil];
[task setArguments: arguments];
// and then do all the other jazz for running an NSTask.
Reference : http://borkware.com/quickies/one?topic=nstask
Now, as for a "proper" command execution function, here's one I'm usually using...
Code :
/*******************************************************
*
* MAIN ROUTINE
*
*******************************************************/
- (void)runCommand:(NSString *)cmd withArgs:(NSArray *)argsArray
{
//-------------------------------
// Set up Task
//-------------------------------
if (task) { [self terminate]; }
task = [[NSTask alloc] init];
[task setLaunchPath:cmd];
[task setArguments:argsArray];
[task setStandardOutput:[NSPipe pipe]];
[task setStandardError:[task standardOutput]];
//-------------------------------
// Set up Observers
//-------------------------------
[PP_NOTIFIER removeObserver:self];
[PP_NOTIFIER addObserver:self
selector:#selector(commandSentData:)
name: NSFileHandleReadCompletionNotification
object: [[task standardOutput] fileHandleForReading]];
[PP_NOTIFIER addObserver:self
selector:#selector(taskTerminated:)
name:NSTaskDidTerminateNotification
object:nil];
//-------------------------------
// Launch
//-------------------------------
[[[task standardOutput] fileHandleForReading] readInBackgroundAndNotify];
[task launch];
}
/*******************************************************
*
* OBSERVERS
*
*******************************************************/
- (void)commandSentData:(NSNotification*)n
{
NSData* d;
d = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem];
if ([d length])
{
NSString* s = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
NSLog(#"Received : %#",s);
}
[[n object] readInBackgroundAndNotify];
}
- (void)taskTerminated:(NSNotification*)n
{
[task release];
task = nil;
}
Your use of the pipe looks correct to me.
I'm not sure why you're using /bin/sh. Just set up the NSTask with its launch path being #"/usr/bin/hdiutil" and with its arguments being an array of #"chpass", #"-oldstdinpass", #"-newstdinpass", and #"/path/to/dmg". This is much safer. For example, what if the path to the dmg contains a character the shell would interpret, like $?
Unless you specifically want to take advantage of a shell feature, don't use the shell.

Running multiple NSTasks consecutively

I need to run multiple commands in sequence using NSTask and was wondering what would be a good way to tell if a task has finished so I can continue on to the next command. I'm using "sox" (which I am including in my application bundle) to create temporary audio files using input audio files, and then need to combine those temporary audio files into a single file. An example of the flow of processes (not the actual commands):
1) songA > tempA
2) songB > tempB
3)combine tempA tempB > songC
I'm using the following code to complete the first command:
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"songA", #"-f", #"-S", #"-G", #"-V", #"-b", #"24", #"-r", #"384k", #"tempA", nil];
NSString *path=[[NSBundle mainBundle] pathForResource:#"sox" ofType:nil];
NSTask *task;
task = [[NSTask alloc] init];
[task setStandardInput:[NSPipe pipe]];
[task setLaunchPath:path];
[task setArguments: arguments1];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *string;
string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog (#"stuff :\n%#", string);
[string release];
[task release];
Suppposing I needed to perform two more NSTask processes after this one had finished (using the output of the previous processs), what would be the best way to detect that one process has finished so that I can continue on to the next one.
Thanks.
Maybe not understand fully, but
[task waitUntilExit];
does not do the job?

NSTask NSPipe - objective c command line help

Here is my code:
task = [[NSTask alloc] init];
[task setCurrentDirectoryPath:#"/applications/jarvis/brain/"];
[task setLaunchPath:#"/applications/jarvis/brain/server.sh"];
NSPipe * out = [NSPipe pipe];
[task setStandardOutput:out];
[task launch];
[task waitUntilExit];
[task release];
NSFileHandle * read = [out fileHandleForReading];
NSData * dataRead = [read readDataToEndOfFile];
NSString * stringRead = [[[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding] autorelease];
So I'm trying to replicate this:
cd /applications/jarvis/brain/
./server.sh
but using NSTask in objective-c.
For some reason though, when I run this code, stringRead, returns nothing. It should return what terminal is returning when I launch the .sh file. Correct?
Any ideas?
Elijah
Xcode Bug
There's a bug in Xcode that stops it from printing any output after a a new task using standard output is launched (it collects all output, but no longer prints anything). You're going to have to call [task setStandardInput:[NSPipe pipe]] to get it to show output again (or, alternatively, have the task print to stderr instead of stdout).
Suggestion for final code:
NSTask *server = [NSTask new];
[server setLaunchPath:#"/bin/sh"];
[server setArguments:[NSArray arrayWithObject:#"/path/to/server.sh"]];
[server setCurrentDirectoryPath:#"/path/to/current/directory/"];
NSPipe *outputPipe = [NSPipe pipe];
[server setStandardInput:[NSPipe pipe]];
[server setStandardOutput:outputPipe];
[server launch];
[server waitUntilExit]; // Alternatively, make it asynchronous.
[server release];
NSData *outputData = [[outputPipe fileHandleForReading] readDataToEndOfFile];
NSString *outputString = [[[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding] autorelease]; // Autorelease optional, depending on usage.
The solution above is freezing because it's synchronous.
Call to [server waitUntilExit] blocks the run loop until the tasks is done.
Here's the async solution for getting the task output.
task.standardOutput = [NSPipe pipe];
[[task.standardOutput fileHandleForReading] setReadabilityHandler:^(NSFileHandle *file) {
NSData *data = [file availableData]; // this will read to EOF, so call only once
NSLog(#"Task output! %#", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
// if you're collecting the whole output of a task, you may store it on a property
[self.taskOutput appendData:data];
}];
Probably you want to repeat the same above for task.standardError.
IMPORTANT:
When your task terminates, you have to set readabilityHandler block to nil; otherwise, you'll encounter high CPU usage, as the reading will never stop.
[task setTerminationHandler:^(NSTask *task) {
// do your stuff on completion
[task.standardOutput fileHandleForReading].readabilityHandler = nil;
[task.standardError fileHandleForReading].readabilityHandler = nil;
}];
This is all asynchronous (and you should do it async), so your method should have a ^completion block.