/core/externals/google-toolbox-for-mac/Foundation/GTMScriptRunner.m
http://macfuse.googlecode.com/ · Objective C · 383 lines · 285 code · 53 blank · 45 comment · 76 complexity · 5831dfe37f8dda112ce3551baedbfcdd MD5 · raw file
- //
- // 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 "GTMScriptRunner.h"
- #import "GTMDefines.h"
- #import <unistd.h>
- #import <fcntl.h>
- #import <sys/select.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;
- }