Cocoa wrapper for an interactive Unix command - objective-c

Ok, so I know you can make an NSTask to run command line tools with Objective-C:
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/usr/bin/gdb"];
[task launch];
I'm just wondering if there's a way to communicate with interactive command line tools such a as gdb. This would involve giving the command inputs based on user interaction (like run, kill or quit with gdb) and then reacting based on the information it outputs.

You can use NSTask's setStandardInput:, setStandardOutput: and setStandardError: selectors in conjunction with NSPipe instances to communicate with the launched program.
For example, to read the task's output:
task = [[NSTask alloc] init];
[task setStandardOutput: [NSPipe pipe]];
[task setStandardError: [task standardOutput]]; // Get standard error output too
[task setLaunchPath: #"/usr/bin/gdb"];
[task launch];
You can then obtain an NSFileHandle instance that you can use to read the task's output with:
NSFileHandle *readFromMe = [[task standardOutput] fileHandleForReading];
To set up a pipe for sending commands to gdb, you would add
[task setStandardInput: [NSPipe pipe]];
before you launch the task. Then you get the NSFileHandle with
NSFileHandle *writeToMe = [[task standardInput] fileHandleForWriting];

Use setStandardInput: and setStandardOutput: methods of NSTaks class.
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/usr/bin/gdb"];
NSPipe *outputpipe=[[NSPipe alloc]init];
NSPipe *errorpipe=[[NSPipe alloc]init];
NSFileHandle *output,*error;
[task setArguments: arguments];
[task setStandardOutput:outputpipe];
[task setStandardError:errorpipe];
NSLog(#"%#",arguments);
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];
-(void) receivedData:(NSNotification*) rec_not {
NSFileHandle *out=[[task standardOutput] fileHandleForReading];
NSData *dataOutput=[[rec_not userInfo] objectForKey:NSFileHandleNotificationDataItem];
if( !dataOutput)
NSLog(#">>>>>>>>>>>>>>Empty Data");
NSString *strfromdata=[[NSString alloc] initWithData:dataOutput encoding:NSUTF8StringEncoding];
[out readInBackgroundAndNotify];
[strfromdata release];
}
/* Called when there is some data in the error pipe */
-(void) receivedError:(NSNotification*) rec_not {
NSFileHandle *err=[[task standardError] fileHandleForReading];
NSData *dataOutput=[[rec_not userInfo] objectForKey:NSFileHandleNotificationDataItem];
if( !dataOutput)
NSLog(#">>>>>>>>>>>>>>Empty Data");
else {
NSString *strfromdata=[[NSString alloc] initWithData:dataOutput encoding:NSUTF8StringEncoding];
[strfromdata release];
}
[err readInBackgroundAndNotify];
}
/* Called when the task is complete */
-(void) TaskCompletion :(NSNotification*) rec_not {
NSLog(#"task ended");
}

Related

See NSTask output Cocoa

How could I make an if statement that's like:
if(the output of a nstask is equal to a specific string){
//do stuff over here
}
I'm running a NSTask and it put's out the data that's coming from it in the NSLog, but how could I not show it there but store it as a NSString or something like that
This is what my task looks like
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/csrutil"];
[task setArguments:#[ #"status" ]];
[task launch];
Any help is greatly appreciated :)
You need a pipe and a file handle to read the result.
Write a method
- (NSString *)runShellScript:(NSString *)cmd withArguments:(NSArray *)args
{
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:cmd];
[task setArguments:args];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file = [pipe fileHandleForReading];
[task launch];
NSData *data = [file readDataToEndOfFile];
return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
}
and call it
NSString *result = [self runShellScript:#"/usr/bin/csrutil" withArguments:#[ #"status" ]];

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];

Getting process output using NSTask on-the-go?

I'd like to get NSTask output as soon as it appears not waiting until process finishes.
I've found this answer but how it should be modified to get data ASAP? I think i should run background thread and wait for output all the time somehow.
You can register for the NSFileHandleDataAvailableNotification notification to read
asynchronously from the task output. Example:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/ls"];
[task setCurrentDirectoryPath:#"/"];
NSPipe *stdoutPipe = [NSPipe pipe];
[task setStandardOutput:stdoutPipe];
NSFileHandle *stdoutHandle = [stdoutPipe fileHandleForReading];
[stdoutHandle waitForDataInBackgroundAndNotify];
id observer = [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification
object:stdoutHandle queue:nil
usingBlock:^(NSNotification *note)
{
// This block is called when output from the task is available.
NSData *dataRead = [stdoutHandle availableData];
NSString *stringRead = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding];
NSLog(#"output: %#", stringRead);
[stdoutHandle waitForDataInBackgroundAndNotify];
}];
[task launch];
[task waitUntilExit];
[[NSNotificationCenter defaultCenter] removeObserver:observer];
Alternatively, you can read on a background thread, for example with GCD:
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/bin/ls"];
[task setCurrentDirectoryPath:#"/"];
NSPipe *stdoutPipe = [NSPipe pipe];
[task setStandardOutput:stdoutPipe];
NSFileHandle *stdoutHandle = [stdoutPipe fileHandleForReading];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSData *dataRead = [stdoutHandle availableData];
while ([dataRead length] > 0) {
NSString *stringRead = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding];
NSLog(#"output: %#", stringRead);
dataRead = [stdoutHandle availableData];
}
});
[task launch];
[task waitUntilExit];

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.

NSTask blocking the main thread

I'm using NSTask, but when I launch the task it blocks the main thread (so I can't update it) until the task ends. This is my code:
NSString *hostsforping = #"google.es";
pingdata = [[NSTask alloc] init];
[pingdata setLaunchPath: #"/sbin/ping"];
NSArray *pingargs;
pingargs = [NSArray arrayWithObjects: #"-c 5", hostsforping, nil];
[pingdata setArguments: pingargs];
NSPipe *pingpipe;
pingpipe = [NSPipe pipe];
[pingdata setStandardOutput: pingpipe];
NSFileHandle *pingfile;
pingfile = [pingpipe fileHandleForReading];
[pingdata launch];
NSData *pingdata1;
pingdata1 = [pingfile readDataToEndOfFile];
NSString *pingstring;
pingstring = [[NSString alloc] initWithData: pingdata1 encoding: NSUTF8StringEncoding];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(taskDidTerminate:)
name:NSTaskDidTerminateNotification
object:nil];
}
- (void) taskDidTerminate:(NSNotification *)notification {
NSLog(#"end");
}
I've been reading that -waitUntilExit does block the main thread, but I'm not using it, so I don't know what I'm doing wrong.
Run the task on a background thread, the readDataToEndOfFile is blocking the main thread.
// Offload the method onto a background thread, could also use Grand Central Dispatch
[self performSelectorInBackground:#selector(startTask) withObject:nil];
- (void)startTask {
NSString *hostsforping = #"google.es";
NSTask *pingdata = [[NSTask alloc] init];
[pingdata setLaunchPath: #"/sbin/ping"];
NSArray *pingargs;
pingargs = [NSArray arrayWithObjects: #"-c 5", hostsforping, nil];
[pingdata setArguments: pingargs];
NSPipe *pingpipe;
pingpipe = [NSPipe pipe];
[pingdata setStandardOutput: pingpipe];
NSFileHandle *pingfile;
pingfile = [pingpipe fileHandleForReading];
[pingdata launch];
NSData *pingdata1;
pingdata1 = [pingfile readDataToEndOfFile];
NSString *pingstring;
pingstring = [[NSString alloc] initWithData: pingdata1 encoding: NSUTF8StringEncoding];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(taskDidTerminate:)
name:NSTaskDidTerminateNotification
object:nil];
}
- (void) taskDidTerminate:(NSNotification *)notification {
// Note this is called from the background thread, don't update the UI here
NSLog(#"end");
// Call updateUI method on main thread to update the user interface
[self performSelectorOnMainThread:#selector(updateUI) withObject:nil waitUntilDone:NO];
}
Since, you use the readDataToEndOfFile, you're implicit saying that your NSTask need a blocking execution. I could suggest you to wrap your code around GCD brackets, using dispatch_async. For example:
- (void)executeShellScript {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSString *execPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"myScript.sh"];
NSPipe *pipe = [NSPipe pipe];
NSFileHandle *file = pipe.fileHandleForReading;
NSTask *task = [[NSTask alloc] init];
task.launchPath = #"/bin/sh";
task.arguments = #[execPath];
task.standardOutput = pipe;
[task launch];
NSData *data = [file readDataToEndOfFile]; //readDataToEndOfFile is blocking the main thread.
[file closeFile];
NSString *bashOutput = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog (#"shell returned:\n%#", bashOutput);
});
}