NSTask for SSH using PTY - objective-c

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.

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

How do I run a binary from a Cocoa app?

I'm making a simple launcher for a binary so I can have it appear in my launchpad.
My initial thought was that running system("./myProgram"); would be sufficient, but it doesn't appear to actually do anything as the terminal instance it runs doesn't stay open after running the command, immediately shutting down whatever other tasks the program did.
So my question is, is there a way for me to do this that keeps it open indefinitely?
Edit: I want my launcher to close immediately after launching the program, so it would be less than ideal to rely on something that requires it to stay open
Edit: the following all work, but only when run from xcode, when running it stand-alone it doesn't launch the program at all
system("open /Applications/Utilities/Terminal.app myProgram");
system("open myProgram");
system("/bin/sh -c ./myProgram&");
system("./myProgram&");
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/bash"];
[task setArguments: #[#"-c", #"./myProgram"]];
[task launch];
NSTask does not give any errors, and it doesn't throw any exceptions either when the app runs
Literally every other aspect of the program works, it just won't launch, and it won't say why
Based on all the "feedback" here's what I got so far. And it still doesn't work unless I provide an absolute path (which is no good in case I want to move it later)
//
// AppDelegate.m
// DFLauncher
//
// Created by Electric Coffee on 11/02/15.
// Copyright (c) 2015 Electric Coffee. All rights reserved.
//
#import "AppDelegate.h"
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#end
NSString *CURRENT_DIR;
NSString *FILE_PATH;
NSString *INIT_PATH = #"/data/init/init.txt";
NSString *VOLUME_ON = #"[SOUND:YES]";
NSString *VOLUME_OFF = #"[SOUND:NO]";
BOOL contains(NSString *a, NSString *b) {
return [a rangeOfString: b].location != NSNotFound;
}
NSData *replaceString(NSString *fileContents, NSString *from, NSString *to) {
return [[fileContents stringByReplacingOccurrencesOfString: from withString: to]
dataUsingEncoding: NSUTF8StringEncoding];
}
#implementation AppDelegate
- (void)applicationDidFinishLaunching: (NSNotification *)aNotification {
CURRENT_DIR = [[NSFileManager new] currentDirectoryPath]; //[[NSBundle mainBundle] bundlePath];
//NSLog(#"%#", CURRENT_DIR);
FILE_PATH = [CURRENT_DIR stringByAppendingString: INIT_PATH];
_fileContents = [NSString stringWithContentsOfFile: FILE_PATH
encoding: NSUTF8StringEncoding
error: NULL];
if (contains(_fileContents, VOLUME_OFF))
[_toggleMute setState: YES];
if (contains(_fileContents, VOLUME_ON))
[_toggleMute setState: NO];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
}
- (IBAction)playButtonClick: (id)sender {
//system("open /Applications/Utilities/Terminal.app df"); // doesn't quite work
//system("open /Applications/df_osx/df");
//system("/bin/sh -c /Applications/df_osx/df&");
//system("/Applications/df_osx/df&");
NSString *gamePath = [CURRENT_DIR stringByAppendingString: #"/df&"];
NSTask *task = [NSTask new];
[task setLaunchPath: #"/bin/bash"];
[task setArguments: #[#"-c", gamePath]];
NSError *error = task.standardError;
[task launch];
[NSAlert alertWithError: error];
//[NSApp terminate: self];
}
- (IBAction)folderButtonClick: (id)sender {
system("open .");
}
- (IBAction)quitButtonClick: (id)sender {
[NSApp terminate: self];
}
- (IBAction)mute: (id)sender {
NSData *result;
NSFileManager *fm = [NSFileManager defaultManager];
if ([sender state] == NSOffState)
result = replaceString(_fileContents, VOLUME_OFF, VOLUME_ON);
else
result = replaceString(_fileContents, VOLUME_ON, VOLUME_OFF);
[fm createFileAtPath: FILE_PATH contents: result attributes: nil];
}
#end
Hacky solution that works (but isn't elegant at all)
I had to replace
FILE_PATH = [CURRENT_DIR stringByAppendingString: INIT_PATH];
With
CURRENT_DIR = [[[[NSBundle mainBundle] bundlePath] stringByDeletingPathExtension] stringByDeletingLastPathComponent];
To get the correct path for the file, but it works now.

echo Does Not Work with NSTask and readInBackgroundAndNotify

I have the following Obj-C code and its log output. Can anyone tell me why I'm not getting any output from the NSFileHandle?
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[self performSelectorInBackground:#selector(startTask:) withObject:nil];
}
- (void) startTask: (id) sender
{
NSPipe *pipe = [[NSPipe alloc] init];
NSFileHandle *fh = pipe.fileHandleForReading;
[fh readInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(output:) name:NSFileHandleReadCompletionNotification object:fh];
NSTask *echoTask = [[NSTask alloc] init];
echoTask.standardOutput = pipe;
echoTask.standardError = [[NSPipe alloc] init];
echoTask.launchPath = #"/bin/echo";
echoTask.arguments = #[#"hello world!"];
NSLog(#"launching...");
[echoTask launch];
[echoTask waitUntilExit];
NSLog(#"finished.");
}
- (void) output:(NSNotification *)notification
{
NSFileHandle *fh = notification.object;
NSLog(#"fh: %#", fh);
NSString *output = [[NSString alloc] initWithData:[fh readDataToEndOfFile] encoding:NSUTF8StringEncoding];
NSLog(#"output: '%#'", output);
}
#end
log:
2014-12-16 10:19:58.154 SubProcess2[14893:704393] launching...
2014-12-16 10:19:58.165 SubProcess2[14893:704393] fh: <NSConcreteFileHandle: 0x6080000e9e80>
2014-12-16 10:19:58.165 SubProcess2[14893:704393] output: ''
2014-12-16 10:19:58.166 SubProcess2[14893:704393] finished.
If I do it synchronously or using the approach in https://stackoverflow.com/a/16274541/1015200 I could get it to work.
Any other techniques and variations(such as launching task without performSelectorInBackground) have failed.
I really want to see if I can get it to work using the notification.
So if I can get any help that'd be great.
The data that has already been read is passed to the notification in the userInfo dictionary under the key NSFileHandleNotificationDataItem, you should be accessing that and not attempting to read further data. E.g. something like:
- (void) output:(NSNotification *)notification
{
NSString *output = [[NSString alloc]
initWithData:notification.userInfo[NSFileHandleNotificationDataItem]
encoding:NSUTF8StringEncoding];
NSLog(#"output: '%#'", output);
}
HTH

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