// // 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 #import #import #import // 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; }