Cocoa code for launching an app with parameters - objective-c

I want a Cocoa equivalent of the command line tool open(1), especially for the usage in this form:
open -a <SomeApplication> <SomeFile> --args <Arg1> <Arg2> ...
Take QuickTime as an example. The following command line will open a file using QuickTime, and the arguments can control if QuickTime plays the file on startup.
open -a "QuickTime Player" a.mp4 --args -MGPlayMovieOnOpen 0 # or 1
I have read Launching an Mac App with Objective-C/Cocoa. prosseek's answer, which I think is equivalent to open -a <App> <File>, works well when I do not specify any argument. ughoavgfhw's answer, which I think is equivalent to open -a <App> --args <Arg1> <Arg2> ..., works well when I do not specify any file to open. But neither can specify a filename and arguments at the same time.
I have also tried to append the filename to the argument list, which is the common way used by unix programs. It seems that some applications can accept it, but some cannot. QuickTime prompts an error saying it cannot open the file. I am using the following code.
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
NSURL *url = [NSURL fileURLWithPath:[workspace fullPathForApplication:#"QuickTime Player"]];
NSArray *arguments = [NSArray arrayWithObjects:#"-MGPlayMovieOnOpen", #"0", #"a.mp4", nil];
[workspace launchApplicationAtURL:url options:0 configuration:[NSDictionary dictionaryWithObject:arguments forKey:NSWorkspaceLaunchConfigurationArguments] error:nil];
// open -a "QuickTime Player" --args -MGPlayMovieOnOpen 0 a.mp4
It seems that the mechanism in opening files differs from usual arguments. Can anyone explain the internals of open(1), or just give me a solution? Thanks.

You might want to pipe the output of the task so you know the results. "a.mp4" needs to be the full path to the file.
NSArray *args = [NSArray arrayWithObjects:#"-a", #"QuickTime Player", #"--args", #"a.mp4", nil];
NSTask *task = [NSTask new];
[task setLaunchPath:#"/usr/bin/open"];
[task setArguments:args];
[task launch];

Related

Run NSTask from .command

Sorry, english is not my first language, and I have a poor grammar skill.
Hi, I am a developer that is very new to Objective-C, and I have some problems with using NSTask. I have a .command file that I want to execute in this cocoa application, but if I use the "open" command using NSTask to execute the file, it launches terminal. Is there a way to just execute it without launching it, like a typical NSTask? Or can I just have a text file with the command to be executed? Thank You.
Here is my code...
NSString *pathtorb = [[NSBundle mainBundle] pathForResource:#"rightSpace" ofType:#"command"];
NSTask *DfileBlankSpace = [[NSTask alloc]init];
DfileBlankSpace.launchPath = #"/usr/bin/open";
DfileBlankSpace.arguments = #[pathtorb];
[DfileBlankSpace launch];
[DfileBlankSpace waitUntilExit];
NSTask *killDock = [[NSTask alloc]init];
killDock.launchPath = #"/usr/bin/killall";
killDock.arguments = #[#"Dock"];
[killDock launch];
[killDock waitUntilExit];
A .command is just a shell script, instead of using open - which associates these files with Terminal - run sh passing it -c and the file pathname as arguments:
DfileBlankSpace.launchPath = #"/bin/sh";
DfileBlankSpace.arguments = #[#"-c", pathtorb];
HTH
Addendum
As #KenThomases as pointed out in the comments, if you have not escaped pathorb to make it a valid bash pathname then you should omit the #"-c" argument. This has the shell read from the file without parsing its file name as though typed on the command line.

NSTask breaks logging in Xcode 5.1.1

I'm using NSTask to run some bash commands, and have been for quite a while. Lately however, any time I try to do this, normal logging stops in the Xcode console (e.g. from NSLog or Cocoa Lumberjack). The console still shows any output that the bash commands may have, but no further logs appear.
The logs will be appearing as normal, and will stop as soon as I execute:
NSTask *someTask = [NSTask new];
NSArray *inputArgs = #[#"-l", #"-c", script];
[someTask setLaunchPath:#"/bin/bash"];
[someTask setArguments:inputArgs];
[someTask launch] // This is when the logs stop
This problem seems to have started with the upgrade to Xcode 5.1.1. Any ideas on how to bring back my lovely messages?
So this seems to be the resurgence of an old bug with Xcode. I found an old answer describing it.
In short I had to change my code to:
NSTask *someTask = [NSTask new];
NSArray *inputArgs = #[#"-l", #"-c", script];
[someTask setLaunchPath:#"/bin/bash"];
[someTask setArguments:inputArgs];
[someTask setStandardInput:[NSPipe pipe]]; // <-- Added this line.
[someTask launch]

Run user defined command with NSTask

I would like to execute a terminal command specified by the user. For example, the user might write killall "TextEdit" or say "Hello world!" in a text field, and I want to execute that command.
NSTask is the way to go, except I have two problems with it:
First: the arguments. Right now I'm doing this:
NSArray* args = [commandString componentsSeparatedByString: #" "];
[task setArguments: [args subarrayWithRange: NSMakeRange(1, [args count] - 1)]]; // First one is the command name
Is this the way to do it? I don't think I've had problems with this yet, but I doesn't look like it's safe. Imagine this: the user writes killall 'Address Book' but the command receives as arguments 'Address and Book'?? That doesn't work. So, what should I do instead? How can I safely parse the arguments?
Second: the launch path. It's much more user-friendly to only have to write the name of the command, instead of the complete path to it. So I want to support that, which means finding out programmatically the full path for a command having only it's name. For that I wrote a category on NSTask like this:
+ (NSString*)completePathForExec: (NSString*)exec
{
NSTask* task = [[NSTask alloc] init];
NSPipe* pipe = [[NSPipe alloc] init];
NSArray* args = [NSArray arrayWithObject: exec];
[task setLaunchPath: #"/usr/bin/which"];
[task setArguments: args];
[task setStandardOutput: pipe];
[task setStandardError: pipe];
[task launch];
[task waitUntilExit];
NSFileHandle* file = [pipe fileHandleForReading];
NSString* result = [[NSString alloc] initWithData: [file readDataToEndOfFile] encoding: NSASCIIStringEncoding];
if ([result length]) {
if ([result hasSuffix: #"\n"]) { result = [result substringWithRange: NSMakeRange(0, [result length] - 1)]; }
return result;
}
else { return exec; }
}
This seems to works well. However, how can I be sure that this path: /usr/bin/which will always work? I mean: will it work on 10.6, 10.7, 10.8, etc? I think I had a problem once where the path to a shell command changed with the system version, and you can never be too careful.
If the path is guaranteed to stay the same, then this isn't a problem. If it changes, then how can I know the 'path to the path-finder'?
It'll be far easier for you to not re-invent the command line parsing wheel. But, of course, going down the route of executing arbitrary user entered code is a security nightmare (tempered by the fact that the user has access to the system and, thus, could probably just run Terminal directly).
Specifically, have NSTask wrap an invocation of one of the shells with the command line option to have it execute an arbitrary string.
sh -c "ls -alF"
This would allow you to pass the path to sh as your launch path, which is in a fixed location on every system. The #"-c" argument tells sh to parse the next argument as a script and, of course, the next argument is whatever the user entered.
Note, this will also give the user the ability to pipe stuff, too.

InterProcess Communication on MacOSX Lion

I'm trying to figure out how to set up IPC between my custom app and a pre-made program.
I'm using MacOSX Lion 10.7.2 and Xcode 4.2.1.
It doesn't matter actually what program exactly, since I believe that a similar reasoning may be applied to any kind of external process.
For testing purposes I'm using a simple bash script:
#test.sh
echo "Starting"
while read out
do
echo $out
done
What I would like to achieve is to redirect input and output of this script, using my app to send inputs to it and read its outputs.
I tried to use NSTask,NSPipe and NSFileHandle as follows:
-(void)awakeFromNib {
task = [[NSTask alloc] init];
readPipe = [NSPipe pipe];
writePipe = [NSPipe pipe];
[task setStandardOutput:readPipe];
[task setStandardInput:writePipe];
[task setLaunchPath:#"/path/test.sh"];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(read:)
name:NSFileHandleReadCompletionNotification
object:nil];
[[readPipe fileHandleForReading] readInBackgroundAndNotify];
[task launch];
}
-(IBAction)write:(id)sender {
NSLog(#"Write called: %d %#\n",[task isRunning],writePipe);
NSFileHandle *writeHandle = [writePipe fileHandleForWriting];
NSString *message = #"someString";
[writeHandle writeData:[message dataUsingEncoding:NSUTF8StringEncoding] ];
}
-(void)read:(NSNotification*)notification {
NSString *output = [[NSString alloc] initWithData:[[notification userInfo] valueForKey: NSFileHandleNotificationDataItem]
encoding:NSUTF8StringEncoding];
NSLog(#"%#",output);
[output release];
[[notification object] readInBackgroundAndNotify];
}
but I'm able only to read the output of test.sh, not to send it any input.
Actually any other example I saw on the web is pretty similar to my code, so I'm not sure if this issue is due to some mistake(s) of mine or to other issues (like app's sandboxing of MacOS Lion).
I've checked XPC documentation, but, according to my researches, in order to use XPC API to IPC, both sides should connect to the same service.
That's not what I'm looking for since I don't want to alter the script in any way, I just want redirect its input and output.
Is my issue due to the lack of XPC and/or to app's sandboxing?
If yes, is there a way to use XPC without modifying the script?
If no, then may somebody explain me what I'm doing wrong?
You don't need XPC for this. Won't make any difference.
Is your script / external process able to read input when you pipe something to it on the command line
% echo "foobar" | /path/test.sh
?
How much data are you sending to it. Writing will be buffered. IIRC -synchronizeFile will flush buffers -- same as fsync(2).

Write to NSTasks standard input after launch

I am currently trying to wrap my head around the hole NSTask, NSPipe, NSFileHandle business. So I thought I write a little tool, which can compile and run C code. I also wanted to be able to redirect my stdout and stdin to a text view.
Here is what I got so far.
I used code from this post to redirect my stdio: What is the best way to redirect stdout to NSTextView in Cocoa?
NSPipe *inputPipe = [NSPipe pipe];
// redirect stdin to input pipe file handle
dup2([[inputPipe fileHandleForReading] fileDescriptor], STDIN_FILENO);
// curInputHandle is an instance variable of type NSFileHandle
curInputHandle = [inputPipe fileHandleForWriting];
NSPipe *outputPipe = [NSPipe pipe];
NSFileHandle *readHandle = [outputPipe fileHandleForReading];
[readHandle waitForDataInBackgroundAndNotify];
// redirect stdout to output pipe file handle
dup2([[outputPipe fileHandleForWriting] fileDescriptor], STDOUT_FILENO);
// Instead of writing to curInputHandle here I would like to do it later
// when my C program hits a scanf
[curInputHandle writeData:[#"123" dataUsingEncoding:NSUTF8StringEncoding]];
NSTask *runTask = [[[NSTask alloc] init] autorelease];
[runTask setLaunchPath:target]; // target was declared earlier
[runTask setArguments:[NSArray array]];
[runTask launch];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(stdoutDataAvailable:) name:NSFileHandleReadCompletionNotification object:readHandle];
And here the stdoutDataAvailable method
- (void)stdoutDataAvailable:(NSNotification *)notification
{
NSFileHandle *handle = (NSFileHandle *)[notification object];
NSString *str = [[NSString alloc] initWithData:[handle availableData] encoding:NSUTF8StringEncoding];
[handle waitForDataInBackgroundAndNotify];
// consoleView is an NSTextView
[self.consoleView setString:[[self.consoleView string] stringByAppendingFormat:#"Output:\n%#", str]];
}
This Program is working just fine. It is running the C program printing the stdout to my text view and reading "123" from my inputPipe. Like indicated in my comment above I would like to provide the input while the task is running once it is needed.
So there are two questions now.
Is there a way to get a notification as soon as somebody tries to read data from my inputPipe?
If the answer to 1 is no, is there a different approach I can try? Maybe using a class other than NSTask?
Any help, sample code, links to other resources are highly appreciated!
I'm not sure whether you can detect a "pull" on an NSPipe. I do have a vague sense that polling for write-availability with select() or using kqueue to look for I/O availability events on the underlying file descriptor of your NSFileHandle might do the trick, but I'm not very familiar with using those facilities in this way.
Do you have to support arbitrary C programs, or is it a special daemon or something you've developed?
If it's your own program, you could watch for requests for feedback on outputPipe, or just blast input onto the inputPipe as you find out what it is you want to send, and let the C program consume it when it's ready; if it's somebody else's code, you may be able to hook scanf and friends using a link-time method (since it's code you're compiling) like the one described in Appendix A-4 of:
http://www.cs.umd.edu/Library/TRs/CS-TR-4585/CS-TR-4585.pdf
The gist of it is to make a .dylib with your custom I/O functions (which may send some sigil to your app indicating that they need input), link that into the built program, set an environment variable (DYLD_BIND_AT_LAUNCH=YES) for the launched task, and run it. Once you've got those hooks in, you can provide whatever conveniences you want for your host program.