I'm new to objective-C, so please forgive me if I'm missing something. But we all have to start somewhere :)
I have a snippet of code I got from another open source project that executes a command and passes the result to another method. What I need to do is listen for each new line printed to stdout and do something with each line.
The code snippet I'm working with is the following:
NSMutableArray *args = [NSMutableArray array];
NSString *input = [details valueForKey:#"input"];
for (NSString *i in [input componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]) {
[args addObject:i];
}
NSTask *scriptTask = [NSTask new];
NSPipe *outputPipe = [NSPipe pipe];
if ([_NSFileManager() isExecutableFileAtPath:scriptPath] == NO) {
NSArray *chmodArguments = #[#"+x", scriptPath];
NSTask *chmod = [NSTask launchedTaskWithLaunchPath:#"/bin/chmod" arguments:chmodArguments];
[chmod waitUntilExit];
}
[scriptTask setStandardOutput:outputPipe];
[scriptTask setLaunchPath:scriptPath];
[scriptTask setArguments:args];
NSFileHandle *filehandle = [outputPipe fileHandleForReading];
[scriptTask launch];
[scriptTask waitUntilExit];
NSData *outputData = [filehandle readDataToEndOfFile];
NSString *outputString = [NSString stringWithData:outputData encoding:NSUTF8StringEncoding];
if (NSObjectIsNotEmpty(outputString)) {
[self.world.iomt inputText:outputString command:IRCPrivateCommandIndex("privmsg")];
}
So, rather than waiting for the process to complete then doing something with the result, I need to wait for each new line that gets printed by the command to stdout.
My background is mostly in web dev, so I guess if you were using Node.js and event emitters my aim would look similar to the following:
task = new Task("ls");
task.addListener("newline", function(data) {
somethingElse("sendmsg", data);
});
task.run();
Hopefully you understand what I'm tying to achieve. Thanks!
What you can do is add an observer to the NSFileHandle to notify you when it reads something.
example:
[fileHandler readInBackgroundAndNotify];
//Need to set NSTask output pipes
[self.task setStandardOutput: outputPipe];
[self.task setStandardError: outputPipe];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(readFromTask:) name:NSFileHandleReadCompletionNotification object:fileHandler];
-(void)readFromTask:(NSNotification *)aNotifcation;
{
NSData *data = [[aNotifcation userInfo] objectForKey: NSFileHandleNotificationDataItem];
if (data != 0) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// Do something with the text
[text release];
[[aNotifcation object] readInBackgroundAndNotify];
}
}
Then there's a notififcation you can add to the task to indicate when its completed so you can do clean up.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(taskEnded:) name:NSTaskDidTerminateNotification object:task];
- (void)taskEnded:(NSNotification *) aNotifcation
{
NSData *data = [[aNotifcation userInfo] objectForKey: NSFileHandleNotificationDataItem];
if (data != 0) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[text release];
}
[self.task release];
}
So from your code, you could remove:
[scriptTask waitUntilExit];
And move the data handling into the notifications.
NSData *outputData = [filehandle readDataToEndOfFile];
NSString *outputString = [NSString stringWithData:outputData encoding:NSUTF8StringEncoding];
if (NSObjectIsNotEmpty(outputString)) {
[self.world.iomt inputText:outputString command:IRCPrivateCommandIndex("privmsg")];
}
That's a rough idea, there's a good post at
http://www.cocoabuilder.com/archive/cocoa/306145-nstask-oddity-with-getting-stdout.html
Related
I am trying to keep my application in dock, however I am not able to do this.
How I am trying to do?
#interface UserDefaultsHelper : NSUserDefaults
- (BOOL)addApplicationToDock:(NSString *)path;
- (BOOL)removeApplicationFromDock:(NSString *)name;
#end
#implementation UserDefaultsHelper
- (BOOL)addApplicationToDock:(NSString *)path {
NSDictionary *domain = [self persistentDomainForName:#"com.apple.dock"];
NSArray *apps = [domain objectForKey:#"persistent-apps"];
NSArray *matchingApps = [apps filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K CONTAINS %#", #"tile-data.file-data._CFURLString", path]];
if ([matchingApps count] == 0) {
NSMutableDictionary *newDomain = [domain mutableCopy];
NSMutableArray *newApps = [apps mutableCopy];
NSDictionary *app = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObjectsAndKeys:path, #"_CFURLString", [NSNumber numberWithInt:0], #"_CFURLStringType", nil] forKey:#"file-data"] forKey:#"tile-data"];
[newApps addObject:app];
[newDomain setObject:newApps forKey:#"persistent-apps"];
[self setPersistentDomain:newDomain forName:#"com.apple.dock"];
return [self synchronize];
}
return NO;
}
- (BOOL)removeApplicationFromDock:(NSString *)name {
NSDictionary *domain = [self persistentDomainForName:#"com.apple.dock"];
NSArray *apps = [domain objectForKey:#"persistent-apps"];
NSArray *newApps = [apps filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"not %K CONTAINS %#", #"tile-data.file-data._CFURLString", name]];
if (![apps isEqualToArray:newApps]) {
NSMutableDictionary *newDomain = [domain mutableCopy];
[newDomain setObject:newApps forKey:#"persistent-apps"];
[self setPersistentDomain:newDomain forName:#"com.apple.dock"];
return [self synchronize];
}
return NO;
}
#end
I found the above code from here. Code Source
the above code is getting compiled and running, however it's adding the application in the application's dock.
I am a newbie here. Please be lenient towards me as I do not have OS level of knowledge.
Many thanks in advance for your help and attention.
The code provided in question is correct the only problem was that the dock was not getting refreshed. So we just need to refresh the DOCK
In order to refresh dock I am calling a terminal command from obj-c
Terminal Command killall dock
....code...
[self runCommand:#"killall Dock"];
}
-(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;
}
I am new developing with Objective-C and Cocoa.
I need to run/stop/restart Apache and Mysql from a Xcode/Cocoa App and I don't know which is the best practise: Using NSTask, using NSAppleScript or what should be used?
Anyway I think that I need admin privileges for that.
Maybe somebody can help me how or where to get starting with this using best practices.
Below it's little of what I already tried:
I also saw this tutorial: http://www.michaelvobrien.com/blog/2009/07/authorizationexecutewithprivileges-a-simple-example/ but the AuthorizationExecuteWithPrivileges method is deprecated from OS 10.6.
I tried several methods that I find on stackoverflow.com but without any result.
One of the ways I tried is with the following method that I have in a TasksController class:
- (BOOL) runProcessAsAdministrator:(NSString*)scriptPath
withArguments:(NSArray *)arguments
output:(NSString **)output
errorDescription:(NSString **)errorDescription {
NSLog(#"runProcessAsAdministrator called!");
NSString * allArgs = [arguments componentsJoinedByString:#" "];
NSString * fullScript = [NSString stringWithFormat:#"%# %#", scriptPath, allArgs];
NSDictionary *errorInfo = [NSDictionary new];
NSString *script = [NSString stringWithFormat:#"do shell script \"sudo %#\" with administrator privileges", fullScript];
NSLog(#"shell script path: %#",script);
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor * eventResult = [appleScript executeAndReturnError:&errorInfo];
// Check errorInfo
if (! eventResult)
{
// Describe common errors
*errorDescription = nil;
if ([errorInfo valueForKey:NSAppleScriptErrorNumber])
{
NSNumber * errorNumber = (NSNumber *)[errorInfo valueForKey:NSAppleScriptErrorNumber];
if ([errorNumber intValue] == -128)
*errorDescription = #"The administrator password is required to do this.";
}
// Set error message from provided message
if (*errorDescription == nil)
{
if ([errorInfo valueForKey:NSAppleScriptErrorMessage])
*errorDescription = (NSString *)[errorInfo valueForKey:NSAppleScriptErrorMessage];
}
return NO;
}
else
{
// Set output to the AppleScript's output
*output = [eventResult stringValue];
return YES;
}
}
Called as following:
TasksController *tasks = [[TasksController alloc] init];
NSString * output = nil;
NSString * processErrorDescription = nil;
BOOL success = [tasks runProcessAsAdministrator:#"/Applications/MyAPP/Library/bin/httpd"
withArguments:[NSArray arrayWithObjects:#"-f /Applications/MyAPP/conf/apache/MyAPP-httpd.conf", #"-k start", nil]
output:&output
errorDescription:&processErrorDescription
];
if (!success) // Process failed to run
{
// ...look at errorDescription
[_lblStatus setStringValue:#" Apache could not be started!"];
[_txtLogs setStringValue:processErrorDescription];
NSLog(#"%#",output);
NSLog(#"%#",processErrorDescription);
}
else
{
// ...process output
[_lblStatus setStringValue:#" Hurray Apache running!!"];
[_txtLogs setStringValue:output];
}
When I tried the above method the app is popup a small window asking for admin password but after introducing pass it's try to run and I get the following message in console:
dyld: DYLD_ environment variables being ignored because main executable (/usr/libexec/security_authtrampoline) is setuid or setgid
I also tried using the following methods but without success:
-(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;
}
-(void) runScript:(NSString*)scriptName
{
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments;
NSString* newpath = [NSString stringWithFormat:#"'%#'/%#",[[NSBundle mainBundle] resourcePath], scriptName];
NSLog(#"shell script path: %#",newpath);
arguments = [NSArray arrayWithObjects:newpath, nil];
[task setArguments: arguments];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *string;
string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog (#"script returned:\n%#", string);
}
For this last method I have ShellScript folder in my Supporting Files folder that contain scripts like startMysql.sh, startApache.sh ...
startMysql.sh example:
# /bin/sh
/Applications/MyAPP/Library/bin/mysqld_safe --port=8889 --socket=/Applications/MyAPP/tmp/mysql/mysql.sock --lower_case_table_names=0 --pid-file=/Applications/MyAPP/tmp/mysql/mysql.pid --log-error=/Applications/MyAPP/logs/mysql_error_log &
Thanks in advance!
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);
});
}
I'm having an issue with NSFileHandleReadCompletionNotification not posting. I'm piping the results of one NSTask into another one. For whatever reason my getData method just will not be called. Any ideas? Both tasks are working properly because I'm getting the expected files written to my hard drive. Just no notification.
- (void) encodeVideo:(NSString*)path
{
NSString* ffmpegPath = [[NSBundle mainBundle] pathForResource:#"ffmpeg" ofType:#""];
NSString* presetFile = [[NSBundle mainBundle] pathForResource:#"128" ofType:#"ffpreset"];
NSTask* encodeTask = [[NSTask alloc] init];
NSMutableArray* arguments = [[NSMutableArray alloc] init];
[arguments addObject:#"-y"];
[arguments addObject:#"-i"];
[arguments addObject:path];
[arguments addObject:#"-f"];
[arguments addObject:#"mpegts"];
[arguments addObject:#"-ac"];
[arguments addObject:#"2"];
[arguments addObject:#"-fpre"];
[arguments addObject:presetFile];
[arguments addObject:#"-s"];
[arguments addObject:#"320x240"];
[arguments addObject:#"-aspect"];
[arguments addObject:#"320:240"];
[arguments addObject:#"-async"];
[arguments addObject:#"2"];
[arguments addObject:#"-"];
[encodeTask setLaunchPath: ffmpegPath];
[encodeTask setArguments: arguments];
[encodeTask setStandardOutput:[NSPipe pipe]];
NSMutableArray* segmentArguments = [[NSMutableArray alloc] init];
[segmentArguments addObject:#"-t"];
[segmentArguments addObject:#"10"];
[segmentArguments addObject:#"-f"];
[segmentArguments addObject:#"/Users/Morgan/Desktop/LiveStreamServer/video"];
[segmentArguments addObject:#"-S"];
[segmentArguments addObject:#"1"];
NSString* segmenterPath = [[NSBundle mainBundle] pathForResource:#"mediastreamsegmenter" ofType:#""];
NSLog(#"%#", segmenterPath);
NSTask* segmentTask = [[NSTask alloc] init];
[segmentTask setLaunchPath: segmenterPath];
[segmentTask setArguments: segmentArguments];
[segmentTask setStandardInput:[encodeTask standardOutput]];
[segmentTask setStandardOutput:[NSPipe pipe]];
[segmentTask setStandardError:[NSPipe pipe]];
NSFileHandle *file = [[segmentTask standardOutput] fileHandleForReading];
[file readInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(getData:) name:NSFileHandleReadCompletionNotification object:file];
[segmentTask launch];
[encodeTask launch];
[arguments release];
[segmentArguments release];
}
- (void) getData: (NSNotification *)aNotification
{
NSData *data = [[aNotification userInfo] objectForKey:#"NSFileHandleNotificationDataItem"];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", string);
[[aNotification object] readInBackgroundAndNotify];
}
Needed to read the stdError in the background as well. Had it outputting into a pipe, but wasn't reading it so the buffer got filled up.
I have an NSTextView that I'm outputting text from NSTask. Everything works as expected except the scrolling and selecting behaviors.
1: If I try to scroll up, the position of my scroll snaps back to the bottom instantly after I let go. Any ideas? I've looked through quite a bit of documentation about this and can't find anything about it.
2: If I select text, it removes it. I just want it to select so I can copy and paste. Lost on this one too.
Any tips or pointers would be most welcome. Thanks.
- (id)init
{
[super init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(readPipe:)
name:NSFileHandleReadCompletionNotification
object:nil];
return self;
}
- (void)kicked
{
task = [[NSTask alloc] init];
[task setLaunchPath:[self.kickLocationTextField stringValue]];
[task setArguments:kickBuild];
NSPipe *pipe = [[NSPipe alloc] init];
fileHandle = [pipe fileHandleForReading];
[fileHandle readInBackgroundAndNotify];
[task setStandardOutput:pipe];
[task setStandardError:pipe];
[task launch];
[task release];
[pipe release];
}
- (void)readPipe:(NSNotification *)notification
{
NSData *data;
NSString *text;
if( [notification object] != fileHandle )
{
return;
}
data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
text = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
[nsTaskOutput insertText:text];
[text release];
if (data != 0)
{
[fileHandle readInBackgroundAndNotify];
}
}
Try this instead of insertText::
NSScroller *scroller = nTaskOutput.enclosingScrollView.verticalScroller;
BOOL shouldScrollToBottom = scroller.doubleValue == 1.0;
NSTextStorage *ts = nTaskOutput.textStorage;
[ts replaceCharactersInRange:NSMakeRange(ts.length, 0) withString:text];
if (shouldScrollToBottom) {
NSRect bounds = nTaskOutput.bounds;
[nTaskOutput scrollPoint:NSMakePoint(NSMaxX(bounds), NSMaxY(bounds))];
}