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
Related
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];
}
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.
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.
So, I managed to get NSTask to read asynchronously from a program, but I did it inside the class of a UIView in my storyboard. (Not an Obj-C expert)
My ideia is: I read the text from the program place it on a UITextView and then when there's more repeat the process via NSNotificationCenter
So far this is my code:
LView.m:
- (void)viewDidLoad
{
[super viewDidLoad];
NSPipe *out_pipe = [NSPipe pipe];
sshoutput = [out_pipe fileHandleForReading];
[sshoutput readInBackgroundAndNotify];
utilT = [[NSTask alloc] init];
[utilT setLaunchPath:#"/usr/bin/utilfc9"];
[utilT setArguments:[NSArray arrayWithObjects: #"-p", #"-f", #"log.txt", nil]];
[utilT setStandardOutput: out_pipe];
[utilT setStandardError: out_pipe];
[utilT launch];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readPipe:) name:NSFileHandleReadCompletionNotification object:nil];
}
-(void)readPipe: (NSNotification *)notification
{
NSData *data;
NSString *new_input;
if( [notification object] != sshoutput ) { return };
data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
new_input = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
self.log.text = [self.wifilog.text stringByAppendingFormat: #"\n%#", new_input];
if( utilT ) {
[sshoutput readInBackgroundAndNotify];
}
}
LView.h:
#import <UIKit/UIKit.h>
#import "NSTask.h"
NSTask *sshT;
NSFileHandle *sshoutput;
So far it works great, I get the data live without any issues.
But, how can I place this NSTask in a more "global" place like AppDelegate's application didFinishLaunchingWithOptions and then process the data and update multiple views in another classes? I tried and of course I can stuff like log.text = new_input inside AppDelegate because it's from another class, and including it does not solve the problem.
As you might noticed, I'm not interested in sending this to the AppStore. It's an app for myself to use on a jailbroken iPhone.
Thank you.
Quick way to do it is
In all of the Views that you want to recieve this same notification add the following
ReceiverView
-(void) viewDidLoad
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(read:) name:#"ReadTest" object:nil];
}
//read function
-(void) read:(NSNotification*)notification
{ // Do something with the notification }
Now in LView.m
-(void)readPipe: (NSNotification *)notification
{
NSData *data;
NSString *new_input;
if( [notification object] != sshoutput ) { return };
data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
new_input = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
self.log.text = [self.wifilog.text stringByAppendingFormat: #"\n%#", new_input];
if( utilT ) {
[sshoutput readInBackgroundAndNotify];
}
//Add the following
[[NSNotificationCenter defaultCenter] postNotificationName:#"ReadTest" object:notification]
}
Be carefull, new_input is allocated but not released => Memory Leak
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;
}];