2

You can run a script in a Cocoa application using the wonderful NSTask, and it works great. The only issue is that I need to run multiple scripts, and in my application, the scripts cannot be combined into one file or one call -- they must be run as separate tasks by the application.

The issue is that apparently you can only run one NSTask in an application. I do not understand why this is the case, but sadly, it seems to be. I have tried everything to debug it, but no matter what the script, how simple or how complicated, my application simply will only execute the first NSTask that I run. This problem has come up before, although less directly, and there has seemingly been no solution.

There has to be a way to run more than one script in an application. Does anyone know a way that I can get around this, or possibly an alternate way to run a script? All I need to do is run a very short bash script that does a "make install".

Here's an example of how I'm running an NSTask, in case it helps.

NSTask *task;
task = [NSTask launchedTaskWithLaunchPath: @"/bin/bash"
                                arguments:[NSArray arrayWithObjects: scriptPath, nil]
        ];

It is indeed working for all of my scripts individually, it just can't run one then another.

Community
  • 1
  • 1
Jeff Escalante
  • 3,137
  • 1
  • 21
  • 30

4 Answers4

5

Sure you can use more than one NSTask. Just use its init method instead of its convenience method, and set the properties manually:

NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/true"];
[task launch];
mipadi
  • 398,885
  • 90
  • 523
  • 479
  • I'm using garbage collection and automatic reference counting here (from xcode4), so I'm not supposed to be using alloc and init... is there any other way I can make this happen? – Jeff Escalante Mar 07 '12 at 18:48
  • 1
    You can most certainly use alloc/init with both garbage collection and ARC. However, there's no reason to use ARC and garbage collection at the same time (will Xcode even let you?). – Andrew Madsen Mar 07 '12 at 19:27
  • Testing this now... in the past when I had used alloc init it threw me an error – Jeff Escalante Mar 09 '12 at 15:23
  • Still doesn't work with alloc init. Can anyone else test this and confirm? – Jeff Escalante Mar 09 '12 at 16:30
  • You absolutely use alloc/init in an ARC app - it's retain/release/autorelease you can't use. What do you mean by "doesn't work"? – zpasternack Mar 10 '12 at 05:41
1

There is some confusion here over "only run once".

NSTask creates a new process, it is that created process which can only be run once; you can use NSTask to create as many different processes as you like, and each one of those can run once. And each of those difference processes you create can execute different, or the same, binaries - so if you want to creates many processes each of which execute /bin/bash you can.

So for each of your scripts create a new NSTask and run it.

Note: the class method launchedTaskWithLaunchPath:arguments creates an NSTask and starts the process running. You can also create an NSTask with [NSTask new] (or [[NSTask alloc] init]), set its parameters, and then launch it.

Note: from your comment on another answer, using [NSTask new] or [[NSTask alloc] init] is normal and you do so whether you are using garbage collection, automatic reference counting, or manual reference counting. It is the methods retain, release and autorelease that you do not use with garbage collection and automatic reference counting.

Response to Comment

user1168440 has shown a more general method of invoking NSTask, but just as confirmation of using the class method here are two shell scripts being executed. First create two scripts in /tmp:

tmp $ cat script1.sh
#!/bin/bash
echo `date -u` script one >>/tmp/script.txt
tmp $ cat script2.sh
#!/bin/bash
echo `date -u` script two >>/tmp/script.txt

And a trivial Obj-C application:

#import "TasksAppDelegate.h"

@implementation TasksAppDelegate

@synthesize window;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
   [NSTask launchedTaskWithLaunchPath:@"/bin/bash" arguments:[NSArray arrayWithObject:@"/tmp/script1.sh"]]; 
   [NSTask launchedTaskWithLaunchPath:@"/bin/bash" arguments:[NSArray arrayWithObject: @"/tmp/script2.sh"]]; 
}

@end

Run in Xcode and check the result:

tmp $ cat script.txt
Sat Mar 10 05:12:43 UTC 2012 script two
Sat Mar 10 05:12:43 UTC 2012 script one

Both tasks have run, so this is not what stopped your code working. It may be that one of your shell scripts needed to read from the standard input, failed, and thus appeared not to run at all?

In general a method which sets up a pipe is capture output (and may supply input) is a far better way to use NSTask, but launchedTaskWithLaunchPath:arguments works fine if capturing output (or supplying input) is not required.

CRD
  • 52,522
  • 5
  • 70
  • 86
  • I would be very tempted to agree with you completely on this one, and it makes a lot of sense. This is how I would expect it to work. But have you tried this? Make two different NSTasks that run two different scripts using the method I did here and try to run them. The first one executes and the second one doesn't, whether you use alloc init or not. If you can post a tested and working example of running two bash scripts, one after another, that would be a life saver – Jeff Escalante Mar 09 '12 at 16:32
  • @JeffEscalante - user1168440 has since posted some code, that should help you. – CRD Mar 09 '12 at 20:53
  • @JeffEscalante - I was intrigued as I usually use `NSTask` with pipes so I tried the simplest `launchedTaskWithLaunchPath:arguments` possible and it works fine, see updated answer. – CRD Mar 10 '12 at 05:24
0

Here is an simple approach that I use to run multiple commands with with NSTask:

First setup a method to accept your commands
-(NSString *) runForOutput:(NSString *)command {

//initialize the command line to run
NSString *runCmd = [[NSString alloc] initWithString:@"/bin/bash"];
NSArray *runArgs = [[NSArray alloc] initWithObjects:@"-c",command,nil];

//update proper label
return [self runThisCmd:runCmd withArgs:runArgs];
}

next setup a method to call NSTask
-(NSString *) runThisCmd:(NSString *) runString withArgs:(NSArray *)runArgs  {

NSTask *task = [NSTask new];
[task setLaunchPath:runString];
[task setArguments:runArgs];
[task setStandardOutput:[NSPipe pipe]];
[task setStandardInput: [NSPipe pipe]];
[task setStandardError: [task standardOutput]];
[task launch];

NSData *stdOuput = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile];
[task waitUntilExit];  
NSString *outputString = [[NSString alloc] initWithData:stdOuput encoding:NSUTF8StringEncoding];
return outputString;
}

Now run a bunch of commands
- (void) runSomething {

    NSString *hostnameLong = [self runForOutput:@"hostname"];
    NSString *hostnameShort = [self runForOutput:@"hostname -s"];
    NSString *startDate = [self runForOutput:@"date '+Start Date: %Y-%m-%d'"];
    NSString *startTime = [self runForOutput:@"date '+Start Time: %H:%M:%S'"];

}

The output of the commands are in the strings. You can work with it from there.
This method is best for short, quick command lines due to the [task waitUntilExit]
call that will block. This works great to simulate what I call "unix one-liners". 
I use it sometimes from awakeFromNib to grab one to two bits of info I may need. 
For long running scripts you would convert to an async approach and use notifications to update variables.
CocoaEv
  • 2,984
  • 20
  • 21
-1

try to use the concept of threading. By assigning each NStask on a different thread you can run two or more NStasks.