Running shell script using NSTask in Mac OS - objective-c

I have written a shell script and the purpose of that script is to capture packet using TCPDUMP and write that capture packet to .pcap file
Script is: TCPDumpScript.sh
echo "peter" | sudo -S tcpdump -i rv0 -n -s 0 -w dumpfile.pcap tcp
I am launching (TCPDumpScript.sh) using NSTask. Getting following output
tcpdump: WARNING: rv0: That device doesn't support promiscuous mode
(BIOCPROMISC: Operation not supported on socket)
tcpdump: WARNING: rv0: no IPv4 address assigned
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on rv0, link-type PKTAP (Packet Tap), capture size 65535 bytes
Problem 1:
Using NSTask. "dumpfile.pcap" is being created.
but when I run the same script using Terminal, its being created with desired captured packets.
Problem 2:
Whenever making changes in TCPDumpScript.sh and then launching script using NSTask. I am getting following error
"launch path not accessible"
But I do via terminal
chmod +x TCPDumpScript.sh
Then I am able to launch this script without any error using NSTask.
So, my question is Can't I create file (Problem 1) "dumpfile.pcap" using NSTask ?
And Do I always require to run permission change command on script file (Problem 2) ?
Please guide me.
(void)runScript:(NSArray*)arguments {
dispatch_queue_t taskQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(taskQueue, ^{
self.isRunning = YES;
#try {
NSString *path = [NSString stringWithFormat:#"%#", [[NSBundle mainBundle] pathForResource:#"TcpDumpScript" ofType:#"sh"]];
self.buildTask = [[NSTask alloc] init];
self.buildTask.launchPath = path;
self.buildTask.arguments = arguments;
// Output Handling
self.outputPipe = [[NSPipe alloc] init];
self.buildTask.standardOutput = self.outputPipe;
[[self.outputPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification object:[self.outputPipe fileHandleForReading] queue:nil usingBlock:^(NSNotification *notification){
NSData *output = [[self.outputPipe fileHandleForReading] availableData];
NSString *outStr = [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding];
dispatch_sync(dispatch_get_main_queue(), ^{
self.outPutTxt.string = [self.outPutTxt.string stringByAppendingString:[NSString stringWithFormat:#"\n%#", outStr]];
// Scroll to end of outputText field
NSRange range;
range = NSMakeRange([self.outPutTxt.string length], 0);
[self.outPutTxt scrollRangeToVisible:range];
});
[[self.outputPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
}];
[self.buildTask launch];
[self.buildTask waitUntilExit];
}
#catch (NSException *exception) {
NSLog(#"Problem Running Task: %#", [exception description]);
}
#finally {
[self.Start setEnabled:YES];
[self.spinner stopAnimation:self];
self.isRunning = NO;
}
});
}

Related

Reading NSTask output persistent memory usage

I have a simple NSTask which runs a shell command, and a NSPipe file handle for reading which is used to read the output of the command and writes out to a file. When this command terminates and the output is written to file, Activity Monitor and Xcode memory monitor shows 700mb of usage which never goes down. I have wrapped the NSData output in an autoreleasepool and the whole task is in it's own function so there is no reason I can see for the high usage when the task is finished. I have profiled this with instruments and there are apparently no leaks and only a few KB of persistent memory after the command runs.
- (void)extractLogs forDate:(NSDate *)date
{
__block NSError* err;
NSDate *collectionEndDate = [[NSCalendar currentCalendar]dateByAddingUnit:NSCalendarUnitDay
value:1
toDate:_collectionStartDate
options:0];
[NSString stringWithFormat:#"%#",collectionEndDate];
NSPipe *pipe = [NSPipe new];
NSTask *logRetrievalTask = [[NSTask alloc]init];
[logRetrievalTask setLaunchPath:#"/bin/bash"];
NSString *cmd = [[NSString alloc]initWithFormat:#"/usr/bin/log show --signpost --no-pager --source --style syslog --start %# --end %# --predicate \"%#\" | %#",
[_collectionStartDate logFormattedDateString],
[collectionEndDate logFormattedDateString],
[NSString stringWithFormat:#"subsystem == '%#'", "com.xxx.xxxxx"],
LOG_TRANSFORM_COMMAND];
[logRetrievalTask setArguments:#[#"-c",
cmd]];
[logRetrievalTask setStandardOutput:pipe];
NSURL *logFileUrl = [self logFilePathForSubsystem:system usingDateString:[_collectionStartDate sduLogFormattedDateString]];
__block FileStreamCompressor *compressor = [[FileStreamCompressor alloc]
initWithFilePath:[logFileUrl path]
maxFileSize:MB_IN_BYTES([self maxFileSize])
currentTotalBytes: _totalLogBytes];
if (compressor == nil)
{
[[self logger]indicateFailureWith:[NSString stringWithFormat:#"Error: Could not create log file for: %#", system]];
return;
}
[logRetrievalTask launch];
NSData *logData = [[pipe fileHandleForReading]readDataToEndOfFile];
[logRetrievalTask terminate];
if ([logData length] == LOG_COMMAND_OUTPUT_HEADER_SIZE)
{
[compressor finishWritingStreamToFile];
[self removeLogFileForSystem:system
usingDateString:[_collectionStartDate logFormattedDateString]
logFileErrorType:#"Empty log file"];
} else {
_totalLogBytes = [compressor compressData:logData error:&err];
if (err != nil) {
[compressor finishWritingStreamToFile];
[[self logger]indicateWarningWith:[err localizedDescription]];
[self removeLogFileForSystem:system
usingDateString:[_collectionStartDate logFormattedDateString]
logFileErrorType:#"Corrupt logfile"];
} else {
[[self logger]indicateSuccessWith:[NSString stringWithFormat:#"Finished processing log: %#-%#.log.gz",
system,
[_collectionStartDate logFormattedDateString]]];
}
}
[[pipe fileHandleForReading]closeFile];
[[pipe fileHandleForWriting]closeFile];
}

NSTask for SSH using PTY

I'm trying to write an app that will programmatically log in to a remote device using SSH much like an expect script (I know I can use expect but I would like to do this in Obj-c).
I have researched a lot on this and know that I need to use a pty. The code I have works fine for telnet but I can't seem to get ssh to work. It seems as though SSH is not using the pty to ask for the password. When I execute the following code I see the device asking for the password, but I don't see my NSLog output.
I'm very new to this and probably over my head, but I'd really appreciate anyone who can help me get this working.
#import <Foundation/Foundation.h>
#import <util.h>
#interface NSTask (PTY)
- (NSFileHandle *)masterSideOfPTYOrError:(NSError **)error;
#end
#implementation NSTask (PTY)
- (NSFileHandle *)masterSideOfPTYOrError:(NSError *__autoreleasing *)error {
int fdMaster, fdSlave;
int rc = openpty(&fdMaster, &fdSlave, NULL, NULL, NULL);
if (rc != 0) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
}
return NULL;
}
fcntl(fdMaster, F_SETFD, FD_CLOEXEC);
fcntl(fdSlave, F_SETFD, FD_CLOEXEC);
NSFileHandle *masterHandle = [[NSFileHandle alloc] initWithFileDescriptor:fdMaster closeOnDealloc:YES];
NSFileHandle *slaveHandle = [[NSFileHandle alloc] initWithFileDescriptor:fdSlave closeOnDealloc:YES];
self.standardInput = slaveHandle;
self.standardOutput = slaveHandle;
return masterHandle;
}
#end
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/ssh"];
[task setArguments:#[#"user#192.168.1.1"]];
NSError *error;
NSFileHandle *masterHandle = [task masterSideOfPTYOrError:&error];
if (!masterHandle) {
NSLog(#"error: could not set up PTY for task: %#", error);
exit(0);
}
[task launch];
[masterHandle waitForDataInBackgroundAndNotify];
NSMutableString *buff = [[NSMutableString alloc] init];
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification
object:masterHandle queue:nil
usingBlock:^(NSNotification *note)
{
NSData *outData = [masterHandle availableData];
NSString *outStr = [[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding];
[buff appendString:outStr];
NSLog(#"output: %#", outStr);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"sername:"
options:NSRegularExpressionCaseInsensitive
error:nil];
NSTextCheckingResult *match = [regex firstMatchInString:buff
options:0
range:NSMakeRange(0, [buff length])];
if (match) {
NSLog(#"got a match!!");
[buff setString:#""];
[masterHandle writeData:[#"bhughes\n" dataUsingEncoding:NSUTF8StringEncoding]];
}
NSLog(#"Exiting function.\n");
[masterHandle waitForDataInBackgroundAndNotify];
}];
[task waitUntilExit];
NSLog(#"Program complete.\n");
}
return 0;
}
As far as I know, NSTask does not support pty ability. Working with something like ssh requires interactive pty device context.
The simplest way to do this is forkpty, but this forks process itself, so it cannot be used with NSTask.
Finally I wrote a wrapper class that manages forkpty. That forks a child process and calls forkpty and execve.
Here's my implementation: https://github.com/eonil/PseudoTeletypewriter.Swift
You can read/write using single master device file handle.
I confirmed that sudo is working, and I believe ssh also should work fine.

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.

Objective C print stdout to UIAlertView

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...

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