Objective C print stdout to UIAlertView - objective-c

I have a C function that prints to stdout using fprintf, and I'm attempting to display the contents of stdout in a UIAlertView. My code is as follows:
NSFileHandle *stdoutFileHandle = [NSFileHandle fileHandleWithStandardOutput];
NSData *stdoutData = [stdoutFileHandle availableData];
NSString *stdoutString = [[NSString alloc] initWithData:stdoutData encoding:NSASCIIStringEncoding];
UIAlertView *stdoutAlert = [[UIAlertView alloc] initWithTitle:#"STDOUT OUTPUT" message:stdoutString delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[stdoutAlert show];
I'm getting the following error when I run my code.
Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '[NSConcreteFileHandle availableData]: Bad file descriptor'
I get an equivalent error when I replace [stdoutFileHandle availableData] with [stdoutFileHandle readDataToEndOfFile].

The problem is you are reading from a output stream. To make this work you need to trick stdout to write it's contents to an input stream instead of to the console.
I know the old C way to do this, but you're not gonna like it. This uses pipe() and dup2().
int pipefd[2];
pipe(pipefd);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
At this point anything written to stdout can be read by pipefd[0]. At that point you can use -initWithFileDescriptor: to read from pipefd[0].
NSFileHandle *stdoutReader = [[NSFileHandle alloc] initWithFileDescriptor:pipefd[0]];
Note, you will want to do lots of error checking. Hope that helps.

I followed the selected answer post in this question:
What is the best way to redirect stdout to NSTextView in Cocoa?
It felt a bit more familiar to follow for me. I created an NSPipe, and NSFileHandler object for the pipe and read handler and I used notifications. I put the open method below in the viewDidAppear and viewDidDisappear method because of my needs but you can put it wherever it's appropriate
// Piping stdout info from here WILL NOT PIPE NSLOG:
// https://stackoverflow.com/questions/2406204/what-is-the-best-way-to-redirect-stdout-to-nstextview-in-cocoa
- (void) openConsolePipe {
_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] ;
}
- (void) closeConsolePipe {
if (_pipe != nil) {
[[_pipe fileHandleForWriting] closeFile];
// documentation suggests don't need to close reading file handle b/c auto but this suggests otherwise:
// https://stackoverflow.com/questions/13747232/using-nstask-and-nspipe-causes-100-cpu-usage
// [[_pipe fileHandleForReading] closeFile];
}
}
- (void) handleNotification:(NSNotification *)notification {
[_pipeReadHandle readInBackgroundAndNotify] ;
NSString *str = [[NSString alloc] initWithData: [[notification userInfo] objectForKey: NSFileHandleNotificationDataItem] encoding: NSUTF8StringEncoding];
// do what you want with the str here.
[_consoleView setText:[_consoleView.text stringByAppendingString:str]];
[_consoleView scrollRangeToVisible:NSMakeRange([_consoleView.text length], 0)];
}
I hope this ends up helping someone else googling this...

Related

Unable to get intermediate output from NSTask's stdout?

I have written an NSTask async exec method for a simple python script.
When then python script just prints to stdout, all is fine.
When there is a raw_input in there (expecting input from the user), it sure gets the input fine, but it does NOT print the data BEFORE raw_input.
What's going on?
- (NSString*)exec:(NSArray *)args environment:(NSDictionary*)env action:(void (^)(NSString*))action completed:(void (^)(NSString*))completed
{
_task = [NSTask new];
_output = [NSPipe new];
_error = [NSPipe new];
_input = [NSPipe new];
NSFileHandle* outputF = [_output fileHandleForReading];
NSFileHandle* errorF = [_error fileHandleForReading];
NSFileHandle* inputF = [_input fileHandleForWriting];
__block NSString* fullOutput = #"";
NSMutableDictionary* envs = [NSMutableDictionary dictionary];
NSArray* newArgs = #[#"bash",#"-c"];
[_task setLaunchPath:#"/usr/bin/env"];
if (env)
for (NSString* key in env)
envs[key] = env[key];
if ([envs count]) [_task setEnvironment:envs];
NSString* cmd = #"";
cmd = [cmd stringByAppendingString:[[[self sanitizedArgs:args] componentsJoinedByString:#" "] stringByAppendingString:#" && echo \":::::$PWD:::::\""]];
[_task setArguments:[newArgs arrayByAddingObject:cmd]];
[_task setStandardOutput:_output];
[_task setStandardError:_error];
[_task setStandardInput:_input];
void (^outputter)(NSFileHandle*) = ^(NSFileHandle *file){
NSData *data = [file availableData];
NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Output: %#",str);
action(str);
fullOutput = [fullOutput stringByAppendingString:str];
};
void (^inputter)(NSFileHandle*) = ^(NSFileHandle *file) {
NSLog(#"In inputter");
NSData *data = [[_task.standardOutput fileHandleForReading] availableData];
NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Output: %#",str);
};
[outputF setReadabilityHandler:outputter];
[errorF setReadabilityHandler:outputter];
//[inputF setWriteabilityHandler:inputter];
[_task setTerminationHandler:^(NSTask* task){
NSLog(#"Terminated: %#",fullOutput);
completed(fullOutput);
//[task.standardOutput fileHandleForReading].readabilityHandler = nil;
//[task.standardError fileHandleForReading].readabilityHandler = nil;
//[task.standardInput fileHandleForWriting].writeabilityHandler = nil;
//[task terminate];
//task = nil;
}];
[_task launch];
//[[_input fileHandleForWriting] waitForDataInBackgroundAndNotify];
return #"";
}
P.S. I've searched everywhere for a solution, but didn't seem to spot anything. It looks like there are tons of NSTask walkthroughs and tutorials, but - funny coincidence - they usually avoid dealing with any of the stdin implications
This doesn't have anything to do with the parent process (the one with the NSTask object). It's all about the behavior of the child process. The child process is literally not writing the bytes to the pipe (yet).
From the stdio man page:
Initially, the standard error stream is unbuffered; the standard input and output
streams are fully buffered if and only if the streams do not refer to an
interactive or “terminal” device, as determined by the isatty(3) function.
In fact, all freshly-opened streams that refer to terminal devices
default to line buffering, and pending output to such streams is written
automatically whenever such an input stream is read. Note that this
applies only to ``true reads''; if the read request can be satisfied by
existing buffered data, no automatic flush will occur. In these cases,
or when a large amount of computation is done after printing part of a
line on an output terminal, it is necessary to fflush(3) the standard
output before going off and computing so that the output will appear.
Alternatively, these defaults may be modified via the setvbuf(3) function.
In your case, the standard input and output do not, in fact, refer to an interactive/terminal device. So, standard output is fully buffered (a.k.a. block buffered, as opposed to line buffered or unbuffered). It is only flushed when the buffer internal to the stdio library is full, when the stream is closed, or when fflush() is called on the stream.
The behavior you're expecting, where standard output is flushed automatically when input is read, only happens in the case where the streams are connected to an interactive/terminal device.
There is no way for the parent process to influence this buffering behavior of the child process except by using a pseudo-terminal device rather than a pipe for the input and output. However, that's a complex and error-prone undertaking. Other than that, the child process has to be coded to set the buffering mode of standard output or flush it regularly.

Handling error when using NSTask

As a learning project Im writing a simple gui for Apache stresstesting command line tool "ab". It requiers a full URL, including a filename such as index.html or simular, as one of its parameters. If a filename is not specified "ab" echos "Invalid url" and shows a lists of available flags.
I would like to catch this "error" and have tried using NSTasks standarderror output. Can´t really get it to work. Would this even classify as an error that would pipe to a standard error?
Besides validating the URL input before launching the NSTask, do you think I can prevent or rather catch this error?
My simple code:
- (void) stressTest:(NSString *)url withNumberOfRequests:(int)requests sendSimultaneously:(int)connections {
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *abPath = [[mainBundle bundlePath] stringByAppendingString:#"/Contents/Resources/ab"];
NSString* requestsStr = [NSString stringWithFormat:#"%i", requests];
NSString* connectionsStr = [NSString stringWithFormat:#"%i", connections];
// Init objects for tasks and pipe
NSTask *abCmd = [NSTask new];
NSPipe *outputPipe = [NSPipe pipe];
[abCmd setLaunchPath:abPath];
[abCmd setArguments:[NSArray arrayWithObjects:#"-n", requestsStr, #"-c", connectionsStr, url, nil]];
[abCmd setStandardOutput:outputPipe];
[abCmd launch];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readCompleted:) name:NSFileHandleReadToEndOfFileCompletionNotification object:[outputPipe fileHandleForReading]];
[[outputPipe fileHandleForReading] readToEndOfFileInBackgroundAndNotify];
}
- (void)readCompleted:(NSNotification *)notification {
NSString * tempString = [[NSString alloc] initWithData:[[notification userInfo] objectForKey:NSFileHandleNotificationDataItem] encoding:NSASCIIStringEncoding];
[resultTextOutlet setString:tempString];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadToEndOfFileCompletionNotification object:[notification object]];
}
ab writes its error messages, including usage information, to standard error. You're currently only reading from standard output. To access the error messages or usage information you'll need to allocate a second NSPipe, pass it to -[NSTask setStandardError:], and then read data from it.

NSTask or AScript?

I am new to objetive-c as I came from Java (Android) and then a little of AppleScript.
So my app is making a git commit. BUT as you know the terminal has output that the user may want to see. So should I continue to use my NSTask for behind the scenes stuff, or should I just use AppleScript and let the user carry on from the terminal. Mainly my push.m looks like this :
#import "Push.h"
#implementation Push
#synthesize test;
#synthesize dirPath;
-(IBAction)chooseFolder:(id)sender{
dirPath = [self get];
NSArray *array = [dirPath componentsSeparatedByString:#"/"];
NSString *pub = [array lastObject];
[projectName setStringValue:pub];
BOOL fileyn = [self check:dirPath];
if(fileyn) {
} else {
}
}
-(IBAction)pushAction:(id)sender {
[self push];
[self push];
}
-(void)push{
if(dirPath == nil || dirPath == #"") {
[self chooseFolder:nil];
}
NSString *commitText = [commitMessage stringValue];
[commitMessage setStringValue:#""];
commitText = [NSString stringWithFormat:#"%#",commitText];
if (i == 1) {
} else {
[self runScript:dirPath:#"add" :#"*" :nil];
[self runScript:dirPath:#"commit" :#"-m" :commitText];
[self runScript:dirPath:#"push" :#"origin" :#"HEAD"];
}
}
-(void) runScript:(NSString *) path:(NSString* )cmd1:(NSString *) cmd2:(NSString *) cmd3{
NSTask *aTask = [[NSTask alloc] init];
NSPipe *pipe;
pipe = [NSPipe pipe];
[aTask setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
NSArray* args = [NSArray arrayWithObjects:cmd1,cmd2,cmd3, nil];
[aTask setArguments:args];
[aTask setCurrentDirectoryPath:path];
[aTask setLaunchPath:#"/usr/local/git/bin/git"];
[aTask setArguments:args];
[aTask launch];
[finished setStringValue:#"finished"];
}
-(IBAction)back:(id)sender{
test = [[NSWindowController alloc] initWithWindowNibName:#"POP"];
[test showWindow:self];
[window close];
}
-(BOOL)check:(NSString *) pow{
BOOL isFile = [[NSFileManager defaultManager] fileExistsAtPath:pow isDirectory:NO];
return isFile;
}
-(NSString *)get {
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
if ([panel runModal] != NSFileHandlingPanelOKButton) return nil;
return [[panel directoryURL] path];
}
#end
So what should I do? Any thing to improve? Thanks in advance!
This sort of application design is really up to you; there's no right answer here. It depends on what you're trying to accomplish, and what you'd like to allow the user to do. If you simply want to show users the output of whatever commands you're running, you can simply get the output out of the NSTask (using the output pipe you set) and display it in a text view for the user to look through.
However, if you want to run interactive git commands, that gets a little bit more complicated. I'm going on a limb here since your question wasn't very specific about what 'let the user carry on from the terminal' means, so if this isn't what you meant, then let me know. In terms of good user interface and user experience, unless you have no other choice, it's almost never a good idea to force someone to go to a different app to keep using your own. If an app wants to display results to a user, it's best to do it in-app (with a custom view, or a web view, for instance), not to drive the user elsewhere. If you want the commands to be interactive, its far better design to come up with an interface for handling that in your app versus running an AppleScript and directing users to the terminal to see and do things.
Anyway, at a quick glance, your code seems like it should work (except for the aforementioned problems in the comments, which you should fix) — and since you hadn't mentioned any problems with it, I assume it does. This isn't a programming problem; it's a design one, and one you'll have to consider the answers to it yourself (and, that being said, I'm voting to close this question since it doesn't really fit into the StackOverflow guidelines — the answer is a matter of opinion, not of facts).

Async execution of shell command not working properly

So, this is my code :
- (void)runCmd:(NSString *)cmd withArgs:(NSArray *)args
{
NSLog(#"\nRunning ::\n\tCmd : %#\n\tArgs : %#",cmd, args);
[theSpinner start];
if (task)
{
[task interrupt];
}
else
{
task = [[NSTask alloc] init];
[task setLaunchPath:cmd];
[task setArguments:args];
[pipe release];
pipe = [[NSPipe alloc] init];
[task setStandardOutput:pipe];
NSFileHandle* fh = [pipe fileHandleForReading];
NSNotificationCenter* nc;
nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self];
[nc addObserver:self
selector:#selector(dataReady:)
name:NSFileHandleReadCompletionNotification
object:fh];
[nc addObserver:self selector:#selector(dataAvailable:) name:NSFileHandleDataAvailableNotification object:fh];
[nc addObserver:self
selector:#selector(taskTerminated:)
name:NSTaskDidTerminateNotification
object:task];
[task launch];
[fh readInBackgroundAndNotify];
}
}
- (void)dataAvailable:(NSNotification*)n
{
NSLog(#"Data Available : %#",n);
}
- (void)dataReady:(NSNotification*)n
{
NSData* d;
d = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem];
NSLog(#"Data Ready : %#",n);
if ([d length])
{
NSLog(#"Data Ready : %#",[[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding]);
}
}
- (void) taskTerminated:(NSNotification*)note
{
NSLog(#"Task Terminated : %#",note);
[task release];
task = nil;
[theSpinner stop];
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
[alert setMessageText:[NSString stringWithFormat:#"Command finished"]];
[alert runModal];
}
I've tried running a command (e.g. the php interpreter at /usr/bin/php) with arguments (e.g. the file to be interpreted by php test.php).
The thing is :
The script runs fine
BUT, I'm receiving a Data Ready and Task Terminated
notification BEFORE I've managed to get all the output. (I mean,
the dataReady: function fetches just the first part of the
output and the rest of it is nowhere to be found...)
I basically want to be reading, asynchronously, all output - WHILE the command is running.
Any Ideas? What am I doing wrong?
Thanks!
You use readInBackgroundAndNotify to schedule your reading. This method reads only one buffer full of data and notifies. You either need to call readInBackgroundAndNotify in your notification method again to read more data or you need to use readToEndOfFileInBackgroundAndNotify if you want to receive all the data at once.
There's a new API since 10.7, so you can avoid using NSNotifications.
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];
}];
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;
}];

Relaunching a cocoa app

I have an application that checks its command line parameters and stores values in persistent stores. One of those is a password that I don't want sticking around for people to see with 'ps' and friends. The approach I'm currently looking at is to, after I've stored the values I need, relaunch the process without the command line parameters. My naive approach is this, where args[0] is the path to the application:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:[args objectAtIndex:0]];
[task launch];
[task release];
[NSApp terminate:nil];
The child is run. However, when my app is terminated the child doesn't seem to orphan but gets stuck. Am I just way off on this one?
More info: So it seems that when I call [NSApp terminate:nil] the NSTask that was launched gets stuck, but if I just exit() then it works fine. However, I'm concerned that things that are open (keychain, plist, etc.) will be in a bad state if I do that.
And note that lots of example code out there is about some watchdog-like process that restarts a separate process when needed. I'm trying to restart the current process that's already running from within that same process.
There are plenty of examples on the web, but this one (also below) looks like it has all the code you need. There are more detailed explanations out there, as well.
// gcc -Wall -arch i386 -arch ppc -mmacosx-version-min=10.4 -Os -framework AppKit -o relaunch relaunch.m
#import <AppKit/AppKit.h>
#interface TerminationListener : NSObject
{
const char *executablePath;
pid_t parentProcessId;
}
- (void) relaunch;
#end
#implementation TerminationListener
- (id) initWithExecutablePath:(const char *)execPath parentProcessId:(pid_t)ppid
{
self = [super init];
if (self != nil) {
executablePath = execPath;
parentProcessId = ppid;
// This adds the input source required by the run loop
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:#selector(applicationDidTerminate:) name:NSWorkspaceDidTerminateApplicationNotification object:nil];
if (getppid() == 1) {
// ppid is launchd (1) => parent terminated already
[self relaunch];
}
}
return self;
}
- (void) applicationDidTerminate:(NSNotification *)notification
{
if (parentProcessId == [[[notification userInfo] valueForKey:#"NSApplicationProcessIdentifier"] intValue]) {
// parent just terminated
[self relaunch];
}
}
- (void) relaunch
{
[[NSWorkspace sharedWorkspace] launchApplication:[NSString stringWithUTF8String:executablePath]];
exit(0);
}
#end
int main (int argc, const char * argv[])
{
if (argc != 3) return EXIT_FAILURE;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[[[TerminationListener alloc] initWithExecutablePath:argv[1] parentProcessId:atoi(argv[2])] autorelease];
[[NSApplication sharedApplication] run];
[pool release];
return EXIT_SUCCESS;
}
I know its a bit late to answer but this answer may help others. Here is a cool trick that can help you.
By using the terminal command, just open your application as a new instance and terminate the current instance.
This is how it is done:
....
NSString* path = [[NSBundle mainBundle] bundlePath];
NSString* cmd = [NSString stringWithFormat:#"open -n %#", path];
[self runCommand:cmd];
exit(0);
}
/// temrinal function
-(NSString*)runCommand:(NSString*)commandToRun;
{
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments = [NSArray arrayWithObjects:
#"-c" ,
[NSString stringWithFormat:#"%#", commandToRun],
nil];
NSLog(#"run command: %#",commandToRun);
[task setArguments: arguments];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *output;
output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
return output;
}
Create an external process that launches yours when it terminates. Then terminate. Launching Cocoa programs with NSTask doesn't work quite right.
For anyone who still wants to use NSTask to relaunch,I found one possible way: Please DO NOT set NSPipe of the NSTask,because the NSTask will terminate the app itself,once the app terminated,the NSTask that started might get stuck there.
At least for me,after I removed the NSPipe settings,my app relaunch successfully.
The following is what I do:
1. Write a command line tool which has 3 parameters: app bundle id,app full path,please note that in this command line you need to terminate the app and wait for a while to make sure it is really terminated before launch the new one,I keep checking app.isTerminated and sleep(1) if it's not terminated.
Launch the Command line tool in app using NSTask,and set the parameters accorddingly,Don't use NSPipe,simply create NSTask and launch
The app relaunches now