blob: 13d03ac32cdc77f2062f7de1c9000d7617f7a7eb [file] [log] [blame]
//
// GTMScriptRunner.m
//
// Copyright 2007-2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//
#import <unistd.h>
#import <fcntl.h>
#import <sys/select.h>
#import <sys/time.h> // Must be include for select() if using modules.
#import "GTMScriptRunner.h"
#import "GTMDefines.h"
static BOOL LaunchNSTaskCatchingExceptions(NSTask *task);
@interface GTMScriptRunner (PrivateMethods)
- (NSTask *)interpreterTaskWithAdditionalArgs:(NSArray *)args;
@end
@implementation GTMScriptRunner
+ (GTMScriptRunner *)runner {
return [[[self alloc] init] autorelease];
}
+ (GTMScriptRunner *)runnerWithBash {
return [self runnerWithInterpreter:@"/bin/bash"];
}
+ (GTMScriptRunner *)runnerWithPerl {
return [self runnerWithInterpreter:@"/usr/bin/perl"];
}
+ (GTMScriptRunner *)runnerWithPython {
return [self runnerWithInterpreter:@"/usr/bin/python"];
}
+ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp {
return [self runnerWithInterpreter:interp withArgs:nil];
}
+ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp withArgs:(NSArray *)args {
return [[[self alloc] initWithInterpreter:interp withArgs:args] autorelease];
}
- (id)init {
return [self initWithInterpreter:nil];
}
- (id)initWithInterpreter:(NSString *)interp {
return [self initWithInterpreter:interp withArgs:nil];
}
- (id)initWithInterpreter:(NSString *)interp withArgs:(NSArray *)args {
if ((self = [super init])) {
trimsWhitespace_ = YES;
interpreter_ = [interp copy];
interpreterArgs_ = [args retain];
if (!interpreter_) {
interpreter_ = @"/bin/sh";
}
}
return self;
}
- (void)dealloc {
[environment_ release];
[interpreter_ release];
[interpreterArgs_ release];
[super dealloc];
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@<%p>{ interpreter = '%@', args = %@, environment = %@ }",
[self class], self, interpreter_, interpreterArgs_, environment_];
}
- (NSString *)run:(NSString *)cmds {
return [self run:cmds standardError:nil];
}
- (NSString *)run:(NSString *)cmds standardError:(NSString **)err {
if (!cmds) return nil;
// Convert input to data
NSData *inputData = nil;
if ([cmds length]) {
inputData = [cmds dataUsingEncoding:NSUTF8StringEncoding];
if (![inputData length]) {
return nil;
}
}
NSTask *task = [self interpreterTaskWithAdditionalArgs:nil];
NSFileHandle *toTask = [[task standardInput] fileHandleForWriting];
NSFileHandle *fromTask = [[task standardOutput] fileHandleForReading];
NSFileHandle *errTask = [[task standardError] fileHandleForReading];
if (!LaunchNSTaskCatchingExceptions(task)) {
return nil;
}
// We're reading an writing to child task via pipes, which is full of
// deadlock dangers. We use non-blocking IO and select() to handle.
// Note that error handling below isn't quite right since
// [task terminate] may not always kill the child. But we want to keep
// this simple.
// Setup for select()
size_t inputOffset = 0;
int toFD = -1;
int fromFD = -1;
int errFD = -1;
int selectMaxFD = -1;
fd_set fdToReadSet, fdToWriteSet;
FD_ZERO(&fdToReadSet);
FD_ZERO(&fdToWriteSet);
if ([inputData length]) {
toFD = [toTask fileDescriptor];
FD_SET(toFD, &fdToWriteSet);
selectMaxFD = MAX(toFD, selectMaxFD);
int flags = fcntl(toFD, F_GETFL);
if ((flags == -1) ||
(fcntl(toFD, F_SETFL, flags | O_NONBLOCK) == -1)) {
[task terminate];
return nil;
}
} else {
[toTask closeFile];
}
fromFD = [fromTask fileDescriptor];
FD_SET(fromFD, &fdToReadSet);
selectMaxFD = MAX(fromFD, selectMaxFD);
errFD = [errTask fileDescriptor];
FD_SET(errFD, &fdToReadSet);
selectMaxFD = MAX(errFD, selectMaxFD);
// Convert to string only at the end, so we don't get partial UTF8 sequences.
NSMutableData *mutableOut = [NSMutableData data];
NSMutableData *mutableErr = [NSMutableData data];
// Communicate till we've removed everything from the select() or timeout
while (([inputData length] && FD_ISSET(toFD, &fdToWriteSet)) ||
((fromFD != -1) && FD_ISSET(fromFD, &fdToReadSet)) ||
((errFD != -1) && FD_ISSET(errFD, &fdToReadSet))) {
// select() on a modifiable copy, we use originals to track state
fd_set selectReadSet;
FD_COPY(&fdToReadSet, &selectReadSet);
fd_set selectWriteSet;
FD_COPY(&fdToWriteSet, &selectWriteSet);
int selectResult = select(selectMaxFD + 1, &selectReadSet, &selectWriteSet,
NULL, NULL);
if (selectResult < 0) {
if ((errno == EAGAIN) || (errno == EINTR)) {
continue; // No change to |fdToReadSet| or |fdToWriteSet|
} else {
[task terminate];
return nil;
}
}
// STDIN
if ([inputData length] && FD_ISSET(toFD, &selectWriteSet)) {
// Use a multiple of PIPE_BUF so that we exercise the non-blocking
// aspect of this IO.
size_t writeSize = PIPE_BUF * 4;
if (([inputData length] - inputOffset) < writeSize) {
writeSize = [inputData length] - inputOffset;
}
if (writeSize > 0) {
// We are non-blocking, so as much as the pipe will take will be
// written.
ssize_t writtenSize = 0;
do {
writtenSize = write(toFD, (char *)[inputData bytes] + inputOffset,
writeSize);
} while ((writtenSize) < 0 && (errno == EINTR));
if ((writtenSize < 0) && (errno != EAGAIN)) {
[task terminate];
return nil;
}
inputOffset += writeSize;
}
if (inputOffset >= [inputData length]) {
FD_CLR(toFD, &fdToWriteSet);
[toTask closeFile];
}
}
// STDOUT
if ((fromFD != -1) && FD_ISSET(fromFD, &selectReadSet)) {
char readBuf[1024];
ssize_t readSize = 0;
do {
readSize = read(fromFD, readBuf, 1024);
} while (readSize < 0 && ((errno == EAGAIN) || (errno == EINTR)));
if (readSize < 0) {
[task terminate];
return nil;
} else if (readSize == 0) {
FD_CLR(fromFD, &fdToReadSet); // Hit EOF
} else {
[mutableOut appendBytes:readBuf length:readSize];
}
}
// STDERR
if ((errFD != -1) && FD_ISSET(errFD, &selectReadSet)) {
char readBuf[1024];
ssize_t readSize = 0;
do {
readSize = read(errFD, readBuf, 1024);
} while (readSize < 0 && ((errno == EAGAIN) || (errno == EINTR)));
if (readSize < 0) {
[task terminate];
return nil;
} else if (readSize == 0) {
FD_CLR(errFD, &fdToReadSet); // Hit EOF
} else {
[mutableErr appendBytes:readBuf length:readSize];
}
}
}
// All filehandles closed, wait.
[task waitUntilExit];
NSString *outString = [[[NSString alloc] initWithData:mutableOut
encoding:NSUTF8StringEncoding]
autorelease];
NSString *errString = [[[NSString alloc] initWithData:mutableErr
encoding:NSUTF8StringEncoding]
autorelease];;
if (trimsWhitespace_) {
NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
outString = [outString stringByTrimmingCharactersInSet:set];
if (err) {
errString = [errString stringByTrimmingCharactersInSet:set];
}
}
// let folks test for nil instead of @""
if ([outString length] < 1) {
outString = nil;
}
// Handle returning standard error if |err| is not nil
if (err) {
// let folks test for nil instead of @""
if ([errString length] < 1) {
*err = nil;
} else {
*err = errString;
}
}
return outString;
}
- (NSString *)runScript:(NSString *)path {
return [self runScript:path withArgs:nil];
}
- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args {
return [self runScript:path withArgs:args standardError:nil];
}
- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args standardError:(NSString **)err {
if (!path) return nil;
NSArray *scriptPlusArgs = [[NSArray arrayWithObject:path] arrayByAddingObjectsFromArray:args];
NSTask *task = [self interpreterTaskWithAdditionalArgs:scriptPlusArgs];
NSFileHandle *fromTask = [[task standardOutput] fileHandleForReading];
if (!LaunchNSTaskCatchingExceptions(task)) {
return nil;
}
NSData *outData = [fromTask readDataToEndOfFile];
NSString *output = [[[NSString alloc] initWithData:outData
encoding:NSUTF8StringEncoding] autorelease];
// Handle returning standard error if |err| is not nil
if (err) {
NSFileHandle *stderror = [[task standardError] fileHandleForReading];
NSData *errData = [stderror readDataToEndOfFile];
*err = [[[NSString alloc] initWithData:errData
encoding:NSUTF8StringEncoding] autorelease];
if (trimsWhitespace_) {
*err = [*err stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
// let folks test for nil instead of @""
if ([*err length] < 1) {
*err = nil;
}
}
[task terminate];
if (trimsWhitespace_) {
output = [output stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
// let folks test for nil instead of @""
if ([output length] < 1) {
output = nil;
}
return output;
}
- (NSDictionary *)environment {
return environment_;
}
- (void)setEnvironment:(NSDictionary *)newEnv {
[environment_ autorelease];
environment_ = [newEnv retain];
}
- (BOOL)trimsWhitespace {
return trimsWhitespace_;
}
- (void)setTrimsWhitespace:(BOOL)trim {
trimsWhitespace_ = trim;
}
@end
@implementation GTMScriptRunner (PrivateMethods)
- (NSTask *)interpreterTaskWithAdditionalArgs:(NSArray *)args {
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath:interpreter_];
[task setStandardInput:[NSPipe pipe]];
[task setStandardOutput:[NSPipe pipe]];
[task setStandardError:[NSPipe pipe]];
// If |environment_| is nil, then use an empty dictionary, otherwise use
// environment_ exactly.
[task setEnvironment:(environment_
? environment_
: [NSDictionary dictionary])];
// Build args to interpreter. The format is:
// interp [args-to-interp] [script-name [args-to-script]]
NSArray *allArgs = nil;
if (interpreterArgs_) {
allArgs = interpreterArgs_;
}
if (args) {
allArgs = allArgs ? [allArgs arrayByAddingObjectsFromArray:args] : args;
}
if (allArgs){
[task setArguments:allArgs];
}
return task;
}
@end
static BOOL LaunchNSTaskCatchingExceptions(NSTask *task) {
BOOL isOK = YES;
@try {
[task launch];
} @catch (id ex) {
isOK = NO;
_GTMDevLog(@"Failed to launch interpreter '%@' due to: %@",
[task launchPath], ex);
}
return isOK;
}