NSTask blocking the main thread - objective-c

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

Related

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

How to start services like Apache and MYSQL from cocoa app on Mac OS 10.8?

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!

NSTask data split by newline \n

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

Cocoa wrapper for an interactive Unix command

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");
}

NSFileHandleReadCompletionNotification not posting

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.