NSTask NSPipe - objective c command line help
Asked Answered
F

2

10

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

Fusil answered 9/8, 2010 at 21:3 Comment(8)
Are you sure the server.sh script outputs on standard out? Maybe you should hook up stderr as well and see if that contains anything. You may also want to consider reading data from the pipe as the task is running, because if it tries to print too much to the pipe while you aren't reading, and the buffer fills up, the task will hang the next time it tries to output anything.Huneycutt
I'm not sure. Can you show me an example? Yes, I removed the [task release] and [task waitUntilExit]. Same problem.Fusil
Are you checking the contents of stringRead programmatically (or in gdb), or are you attempting to print them out using NSLog or something? If you are using NSLog and are seeing no output at all, go check the Console log in Applications > Utilities for your output. Shell scripts run as NSTask can make the Xcode console output stop working. Other than that, I second Kevin's opinion to also check if there's something on standard error (simply add a second pipe and set that as standard error of your task), and to not rely on the pipe being being able to buffer all of your task's output.Log
Programmatically and through NSRunAlertPanel(); just for testing. Ok. I'll try that as well. What other options are there for the output? Can you post any examples that might help?Fusil
And I tried that, same problem....any ideas?Fusil
Put a very simple shell script, e.g. one containing just echo "Hello, World", in place of server.sh. Can you see that output? If you can, how is the output of server.sh done differently?Log
Ok tried entering: "/bin/ls" and it returned correctly. But when I enter "date" it freezes and doesn't get passed the init. Any ideas?Fusil
Echo "hello world" also freezes the appFusil
T
21

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.
Tusche answered 12/8, 2010 at 19:52 Comment(7)
It seems like this is freezing my code as well...huh...any other ideas?Fusil
And, I tried to put in echo "hello world" it returned blank....and date retured blank.Fusil
Even with [server setStandardInput:[NSPipe pipe]]? That's a little bit strange. Did you copy my code verbatim? Try it, and see if it'll work with a file that just contains echo "Hello World"; pwd | ls. If it still doesn't work, please tell us what version of Xcode you're using, and see if writing the string to file produces any output (it might just be the Xcode terminal not printing any more output).Tusche
Years later this bug seems to have resurfaced, and this answer saved me! Thanks!Maines
@PokeyMcPokerson That's great! Glad I could help, preemptively. ;)Tusche
A little late to the party. A couple notes. I'm writing a cli app for mac. The original code was using something like the "async solution". It worked about 95% of the time, but periodically the output would be empty, which broke the app in a really bad way. This version works great. However, don't forget to close the files created with fileHandleForReading if you're running a bunch of procs. In my case, it's written in Kotlin, which has a GC, so the NSTask isn't necessarily deallocated when you'd like. That caused a crash because of excess file handles being open.Raggletaggle
Nothing like a "random" crash to almost ruin the weekend :)Raggletaggle
B
11

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.

Binominal answered 29/4, 2013 at 9:6 Comment(1)
Perfect. Was looking for an example of the block API.Basinger

© 2022 - 2024 — McMap. All rights reserved.