Clang NSTask with streams - objective-c

Never-mind all the "why?","useless?", and "don't bother" comments. I want to compile a program inside another program using clang. I can create the NSTask and set up the arguments and it will work if the file exists, (ie. no stream), and writes to a physical file. I haven't been able to get what I would really like which is to use streams for both input and output. I know that both clang and gcc allow for compiling stdin if you use the -xc and - options but am unable to implement that feature using pipes. I am also not sure how to redirect clang's output to a file handle or stream.
Here is the code I have that compiles it and generates the correct output in outfile
task = [[NSTask alloc] init];
NSPipe* outputPipe = [[NSPipe alloc] init];
[task setStandardOutput:outputPipe ];
[task setStandardError: [task standardOutput]];
NSPipe* inPipe = [NSPipe pipe];
[task setStandardInput:inPipe];
[task setLaunchPath:#"/usr/bin/clang"];
NSString* outfile= [NSString stringWithFormat:#"%#.out",[[filename lastPathComponent] stringByDeletingPathExtension]];
//[data writeToFile:#"file.c" atomically:YES];
[task setArguments:[NSArray arrayWithObjects:filename,#"-S",#"-o",outfile,nil]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(getData:)
name: NSFileHandleReadCompletionNotification
object: [[task standardOutput] fileHandleForReading]];
[[[task standardOutput] fileHandleForReading] readInBackgroundAndNotify];
[task launch];
I have tried using this for the input stream:
/* on pipe creation*/
dup2([[inPipe fileHandleForReading] fileDescriptor], STDIN_FILENO);
NSFileHandle* curInputHandle = [inPipe fileHandleForWriting];
/* tried before launch and after, no output just sits */
[curInputHandle writeData:[NSData dataWithContentsOfFile:filename]];
Sometimes, I assume when the pipe closes while the NSTask still in existance the output file is created and will run. This makes me think that clang is just waiting for stdin to close. Is there a way to close the pipe when the data has been read?
For output I have tried to use NSPipe's fileHandleForWriting as the parameter of -o, That gives an error of [NSConcretePipe fileSystemRepresentation] unrecognized selector. I have tried creating a file handle with the file descriptor of stdout to the same error. I don't know of any command line argument that redirects it. I've tried using | to redirect but haven't been able to get it to work. If there is any unix magic to redirect it I can dup stdout to anywhere I want.
So is there any way to close a pipe when all the data it is read? And Redirect clangs output? If there is any other way to accomplish the same thing easier or cleaner I am open to any implementation.
Any help on these two items would be so great.

It is not clear to me what your problem is or what you've tried. However, if you are going to read the output from a pipe on your main thread using notifications and wish to also write to a pipe one option is to write to the pipe in another thread. The code below, based on your code, does this using GCD. For simplicity in this example the binary is deposited in /tmp:
// send a simple program to clang using a GCD task
- (void)provideStdin:(NSFileHandle *)stdinHandle
{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
[stdinHandle writeData:[#"int main(int argc, char **argv)\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle writeData:[#"{\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle writeData:[#" write(1, \"hello\\n\", 6);\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle writeData:[#"}\n" dataUsingEncoding:NSUTF8StringEncoding]];
[stdinHandle closeFile]; // sent the code, close the file (pipe in this case)
});
}
// read the output from clang and dump to console
- (void) getData:(NSNotification *)notifcation
{
NSData *dataRead = [[notifcation userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *textRead = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding];
NSLog(#"read %3ld: %#", (long)[textRead length], textRead);
}
// invoke clang using an NSTask, reading output via notifications
// and providing input via an async GCD task
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSTask *task = [NSTask new];
NSPipe *outputPipe = [NSPipe new];
[task setStandardOutput:outputPipe];
[task setStandardError:outputPipe];
NSFileHandle *outputHandle = [outputPipe fileHandleForReading];
NSPipe* inPipe = [NSPipe pipe];
[task setStandardInput:inPipe];
[task setLaunchPath:#"/usr/bin/clang"];
[task setArguments:[NSArray arrayWithObjects:#"-o", #"/tmp/clang.out", #"-xc",#"-",nil]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(getData:)
name:NSFileHandleReadCompletionNotification
object:outputHandle];
[outputHandle readInBackgroundAndNotify];
[task launch];
[self provideStdin:[inPipe fileHandleForWriting]];
}

Related

NSTask launch path not accessible. Works in Xcode. Error shown out of XCode

Ok. There are several questions on stack overflow about this. This question was the only question comes closest to mines, but it uses notifications.
The code is very simple. Create a new empty Mac OSX project and just paste the following code in the applicationDidFinishLaunching: method. It supposed to get the path of any executable file (in this case GIT).
NSTask *aTask = [[NSTask alloc] init];
NSPipe *outputPipe = [NSPipe pipe];
NSPipe *errorPipe = [NSPipe pipe];
[aTask setStandardOutput: outputPipe];
[aTask setStandardError: errorPipe];
[aTask setArguments:[NSArray arrayWithObject:#"which"]];
[aTask setLaunchPath:#"/usr/bin/git"];
NSFileHandle *outputFileHandler = [outputPipe fileHandleForReading];
NSFileHandle *errorFileHandler = [errorPipe fileHandleForReading];
[aTask launch];
[aTask waitUntilExit];
// Task launched now just read and print the data
NSData *data = [outputFileHandler readDataToEndOfFile];
NSString *outPutValue = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSData *errorData = [errorFileHandler readDataToEndOfFile];
NSString *errorValue = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
NSLog(#"Error value: %#",errorValue);
NSLog(#"Output Value: %#",outPutValue);
This code sets up two reading pipes and runs one command: which git.
If i run this in XCode i get this results corretly:
Error value: ""
Output Value: /usr/bin/git
If i go to my build/Products/Debug folder and double click the executable file, i get this message printed on the console app:
Question: So, what is really the problem here? please just dont make an alternative solution... I also want to know what the problem is.. thanks.
OK turns out the answer is on stack overflow, but its spread across different questions.
The question was asked here -> Commands with NSTask and here -> NSTask launch path not accessible as well
But their answers as of this date arent clear as to what the problem was. It's only after reading the question from NSTask not picking up $PATH from the user's environment (the question's title was misleading) and with these two answers NSTask not picking up $PATH from the user's environment and Find out location of an executable file in Cocoa that I realized the solution.
It looks like this is about setting up either NS
Task or the user's shell (e.g., ~/.bashrc) so that the correct
environment ($PATH) is seen by NSTask.
Solution:
[task setLaunchPath:#"/bin/bash"];
NSArray *args = [NSArray arrayWithObjects:#"-l",
#"-c",
#"which git", //Assuming git is the launch path you want to run
nil];
[task setArguments: args];
However this assumes the user's shell is always bash and it will fail for others. Solve this by determining the shell.
NSDictionary *environmentDict = [[NSProcessInfo processInfo] environment];
NSString *shellString = [environmentDict objectForKey:#"SHELL"];

NSTask with long output (OSX)

I've been digging for an answer for a long time but I never get to a working code.
I have a code that uses dd to generate a file. Sometimes it takes a few minutes depending on the size and I thought it would be great to have a progress bar. So far everything works and is tied up. The progress bar, however, doesn't update because I need to constantly send values to it. I found a way to get the current value, I managed to get pv to display the current data, but now I can't get the output in real time inside the application, except in the log.
So far, this is the best I got to:
// Action:
// dd if=/dev/zero bs=1048576 count=500 |pv -Wn -s <size>|of=/Users/me/Desktop/asd.img
// Be careful, it generates files of 500MB!!
NSTask * d1Task = [[NSTask alloc] init];
NSTask * pvTask = [[NSTask alloc] init];
NSTask * d2Task = [[NSTask alloc] init];
[d1Task setLaunchPath:#"/bin/dd"];
[pvTask setLaunchPath:#"/Users/me/Desktop/pv"];
[d2Task setLaunchPath:#"/bin/dd"];
// For pvTask I use something like...
// [[NSBundle mainBundle] pathForAuxiliaryExecutable: #"pv"]
// ... in the final version.
[d1Task setArguments: [NSArray arrayWithObjects:
#"if=/dev/zero"
, #"bs=1048576"
, #"count=500"
, nil]];
[pvTask setArguments:[NSArray arrayWithObjects:
#"-Wn"
, [ NSString stringWithFormat:#"-s %d", 1048576 * 500]
, nil]];
[d2Task setArguments: [NSArray arrayWithObjects:
#"of=/Users/me/Desktop/file.dmg"
, nil]];
NSPipe * pipeBetween1 = [NSPipe pipe];
[d1Task setStandardOutput: pipeBetween1];
[pvTask setStandardInput: pipeBetween1];
NSPipe * pipeBetween2 = [NSPipe pipe];
[pvTask setStandardOutput: pipeBetween2];
[d2Task setStandardInput: pipeBetween2];
// Missing code here
[d1Task launch];
[pvTask launch];
[d2Task launch];
In the missing code part, I tried several things. First I tried an observer, like this:
NSFileHandle * pvOutput = [pipeBetween2 fileHandleForReading];
[pvOutput readInBackgroundAndNotify];
[ [NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(outputData:)
name:NSFileHandleDataAvailableNotification
object:pvOutput
];
No success. I get feedback only in the beginning or the end of the execution, but still no feedbacks during it.
I also tried something like:
[pvOutput setReadabilityHandler:^(NSFileHandle *file) {
// Stuff here
}];
But there is no such method here. Maybe my XCode is outdated? (I use 4.2).
Lately I've been trying the same generic code using /sbin/ping pinging 10 times a server, but no success getting the output. How can I do this? Any documents I can read about this subject?
Thanks!
The pv tool writes the progress output to standard error, so you should establish
another pipe:
NSPipe *pvStderrPipe = [NSPipe pipe];
[pvTask setStandardError:pvStderrPipe];
NSFileHandle *pvError = [pvStderrPipe fileHandleForReading];
[pvError waitForDataInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification
object:pvError queue:nil
usingBlock:^(NSNotification *note)
{
NSData *progressData = [pvError availableData];
NSString *progressStr = [[NSString alloc] initWithData:progressData encoding:NSUTF8StringEncoding];
NSLog(#"progress: %#", progressStr);
[pvError waitForDataInBackgroundAndNotify];
}];
A completely different solution might be to use the following feature of "dd":
If dd receives a SIGINFO signal,
the current input and output block counts will be written to the
standard error output in the same format as the standard completion
message.
So you could connect a pipe to stderr of "dd" and call
kill([ddTask processIdentifier], SIGINFO);
at regular intervals. Then you don't need "pv" and probably only one "dd" task.

Code to use NSTask with ps to verify a process is running

I am having endless problems checking to see if the screen saver is running. If I use an NSTask with ps, it crashes or hangs on a lot of users. If I use notifications it seems to be spotty for others.
Any ideas as to why this NSTask is flakey? (Yes, I know it's messy for now as I debug)
-(BOOL)checkScreenSaverRunning
{
MYLog(#"Building task to check screen saver running");
BOOL foundSaver=FALSE;
NSTask *task;
int i;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/ps"];
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"-ax", nil];
[task setArguments: arguments];
NSPipe *stdpipe;
stdpipe = [NSPipe pipe];
[task setStandardOutput: stdpipe];
NSFileHandle *stdfile;
stdfile = [stdpipe fileHandleForReading];
MYLog(#"Launching task to check screen saver running");
[task launch];
while ([task isRunning]){
NSData *stddata;
stddata = [stdfile readDataToEndOfFile];
if([stddata length]>0){
NSString *stdstring = [[NSString alloc] initWithData:stddata
encoding:NSUTF8StringEncoding];
NSArray *stdReturnValues=[stdstring componentsSeparatedByString:#"\n"];
for(i=0;i<[stdReturnValues count];i++){
if([[stdReturnValues objectAtIndex:i]
rangeOfString:#"ScreenSaverEngine"].location != NSNotFound){
foundSaver=TRUE;
MYLog(#"Found screensaver in running processes");
}
}
[stdstring release];
stdstring=nil;
}
}
MYLog(#"Task ended");
[task release];
if(foundSaver)screenSaverIsActive=TRUE;
else screenSaverIsActive=FALSE;
return(foundSaver);
}
What is your higher-level purpose for wanting to know if the screen saver is running? There may be a better way to accomplish that.
If you're trying to diagnose a crash or a hang, show the crash or hang report.
Anyway, if you're going to spawn a subprocess for this, you should probably use killall -0 ScreenSaverEngine instead of ps. killall will find a process by name for you. Using the signal 0 (-0) means "just test for process existence, don't actually signal it". Do [task setStandardError:[NSFileHandle fileHandleWithNullDevice]] to make sure its output goes nowhere. You determine if the process existed by examining the success or failure status of the task after it terminates.

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.

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