PageRenderTime 86ms CodeModel.GetById 30ms app.highlight 52ms RepoModel.GetById 1ms app.codeStats 0ms

/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
  1//
  2//  GTMScriptRunner.m
  3//
  4//  Copyright 2007-2008 Google Inc.
  5//
  6//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
  7//  use this file except in compliance with the License.  You may obtain a copy
  8//  of the License at
  9//
 10//  http://www.apache.org/licenses/LICENSE-2.0
 11//
 12//  Unless required by applicable law or agreed to in writing, software
 13//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 14//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 15//  License for the specific language governing permissions and limitations under
 16//  the License.
 17//
 18
 19#import "GTMScriptRunner.h"
 20#import "GTMDefines.h"
 21#import <unistd.h>
 22#import <fcntl.h>
 23#import <sys/select.h>
 24
 25static BOOL LaunchNSTaskCatchingExceptions(NSTask *task);
 26
 27@interface GTMScriptRunner (PrivateMethods)
 28- (NSTask *)interpreterTaskWithAdditionalArgs:(NSArray *)args;
 29@end
 30
 31@implementation GTMScriptRunner
 32
 33+ (GTMScriptRunner *)runner {
 34  return [[[self alloc] init] autorelease];
 35}
 36
 37+ (GTMScriptRunner *)runnerWithBash {
 38  return [self runnerWithInterpreter:@"/bin/bash"];
 39}
 40
 41+ (GTMScriptRunner *)runnerWithPerl {
 42  return [self runnerWithInterpreter:@"/usr/bin/perl"];
 43}
 44
 45+ (GTMScriptRunner *)runnerWithPython {
 46  return [self runnerWithInterpreter:@"/usr/bin/python"];
 47}
 48
 49+ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp {
 50  return [self runnerWithInterpreter:interp withArgs:nil];
 51}
 52
 53+ (GTMScriptRunner *)runnerWithInterpreter:(NSString *)interp withArgs:(NSArray *)args {
 54  return [[[self alloc] initWithInterpreter:interp withArgs:args] autorelease];
 55}
 56
 57- (id)init {
 58  return [self initWithInterpreter:nil];
 59}
 60
 61- (id)initWithInterpreter:(NSString *)interp {
 62  return [self initWithInterpreter:interp withArgs:nil];
 63}
 64
 65- (id)initWithInterpreter:(NSString *)interp withArgs:(NSArray *)args {
 66  if ((self = [super init])) {
 67    trimsWhitespace_ = YES;
 68    interpreter_ = [interp copy];
 69    interpreterArgs_ = [args retain];
 70    if (!interpreter_) {
 71      interpreter_ = @"/bin/sh";
 72    }
 73  }
 74  return self;
 75}
 76
 77- (void)dealloc {
 78  [environment_ release];
 79  [interpreter_ release];
 80  [interpreterArgs_ release];
 81  [super dealloc];
 82}
 83
 84- (NSString *)description {
 85  return [NSString stringWithFormat:@"%@<%p>{ interpreter = '%@', args = %@, environment = %@ }",
 86          [self class], self, interpreter_, interpreterArgs_, environment_];
 87}
 88
 89- (NSString *)run:(NSString *)cmds {
 90  return [self run:cmds standardError:nil];
 91}
 92
 93- (NSString *)run:(NSString *)cmds standardError:(NSString **)err {
 94  if (!cmds) return nil;
 95
 96  // Convert input to data
 97  NSData *inputData = nil;
 98  if ([cmds length]) {
 99    inputData = [cmds dataUsingEncoding:NSUTF8StringEncoding];
100    if (![inputData length]) {
101      return nil;
102    }
103  }
104
105  NSTask *task = [self interpreterTaskWithAdditionalArgs:nil];
106  NSFileHandle *toTask = [[task standardInput] fileHandleForWriting];
107  NSFileHandle *fromTask = [[task standardOutput] fileHandleForReading];
108  NSFileHandle *errTask = [[task standardError] fileHandleForReading];
109
110  if (!LaunchNSTaskCatchingExceptions(task)) {
111    return nil;
112  }
113
114  // We're reading an writing to child task via pipes, which is full of
115  // deadlock dangers. We use non-blocking IO and select() to handle.
116  // Note that error handling below isn't quite right since
117  // [task terminate] may not always kill the child. But we want to keep
118  // this simple.
119
120  // Setup for select()
121  size_t inputOffset = 0;
122  int toFD = -1;
123  int fromFD = -1;
124  int errFD = -1;
125  int selectMaxFD = -1;
126  fd_set fdToReadSet, fdToWriteSet;
127  FD_ZERO(&fdToReadSet);
128  FD_ZERO(&fdToWriteSet);
129  if ([inputData length]) {
130    toFD = [toTask fileDescriptor];
131    FD_SET(toFD, &fdToWriteSet);
132    selectMaxFD = MAX(toFD, selectMaxFD);
133    int flags = fcntl(toFD, F_GETFL);
134    if ((flags == -1) ||
135        (fcntl(toFD, F_SETFL, flags | O_NONBLOCK) == -1)) {
136      [task terminate];
137      return nil;
138    }
139  } else {
140    [toTask closeFile];
141  }
142  fromFD = [fromTask fileDescriptor];
143  FD_SET(fromFD, &fdToReadSet);
144  selectMaxFD = MAX(fromFD, selectMaxFD);
145  errFD = [errTask fileDescriptor];
146  FD_SET(errFD, &fdToReadSet);
147  selectMaxFD = MAX(errFD, selectMaxFD);
148
149  // Convert to string only at the end, so we don't get partial UTF8 sequences.
150  NSMutableData *mutableOut = [NSMutableData data];
151  NSMutableData *mutableErr = [NSMutableData data];
152
153  // Communicate till we've removed everything from the select() or timeout
154  while (([inputData length] && FD_ISSET(toFD, &fdToWriteSet)) ||
155         ((fromFD != -1) && FD_ISSET(fromFD, &fdToReadSet)) ||
156         ((errFD != -1) && FD_ISSET(errFD, &fdToReadSet))) {
157    // select() on a modifiable copy, we use originals to track state
158    fd_set selectReadSet;
159    FD_COPY(&fdToReadSet, &selectReadSet);
160    fd_set selectWriteSet;
161    FD_COPY(&fdToWriteSet, &selectWriteSet);
162    int selectResult = select(selectMaxFD + 1, &selectReadSet, &selectWriteSet,
163                              NULL, NULL);
164    if (selectResult < 0) {
165      if ((errno == EAGAIN) || (errno == EINTR)) {
166        continue;  // No change to |fdToReadSet| or |fdToWriteSet|
167      } else {
168        [task terminate];
169        return nil;
170      }
171    }
172    // STDIN
173    if ([inputData length] && FD_ISSET(toFD, &selectWriteSet)) {
174      // Use a multiple of PIPE_BUF so that we exercise the non-blocking
175      // aspect of this IO.
176      size_t writeSize = PIPE_BUF * 4;
177      if (([inputData length] - inputOffset) < writeSize) {
178        writeSize = [inputData length] - inputOffset;
179      }
180      if (writeSize > 0) {
181        // We are non-blocking, so as much as the pipe will take will be
182        // written.
183        ssize_t writtenSize = 0;
184        do {
185          writtenSize = write(toFD, (char *)[inputData bytes] + inputOffset,
186                              writeSize);
187        } while ((writtenSize) < 0 && (errno == EINTR));
188        if ((writtenSize < 0) && (errno != EAGAIN)) {
189          [task terminate];
190          return nil;
191        }
192        inputOffset += writeSize;
193      }
194      if (inputOffset >= [inputData length]) {
195        FD_CLR(toFD, &fdToWriteSet);
196        [toTask closeFile];
197      }
198    }
199    // STDOUT
200    if ((fromFD != -1) && FD_ISSET(fromFD, &selectReadSet)) {
201      char readBuf[1024];
202      ssize_t readSize = 0;
203      do {
204        readSize = read(fromFD, readBuf, 1024);
205      } while (readSize < 0 && ((errno == EAGAIN) || (errno == EINTR)));
206      if (readSize < 0) {
207          [task terminate];
208          return nil;
209      } else if (readSize == 0) {
210        FD_CLR(fromFD, &fdToReadSet);  // Hit EOF
211      } else {
212        [mutableOut appendBytes:readBuf length:readSize];
213      }
214    }
215    // STDERR
216    if ((errFD != -1) && FD_ISSET(errFD, &selectReadSet)) {
217      char readBuf[1024];
218      ssize_t readSize = 0;
219      do {
220        readSize = read(errFD, readBuf, 1024);
221      } while (readSize < 0 && ((errno == EAGAIN) || (errno == EINTR)));
222      if (readSize < 0) {
223          [task terminate];
224          return nil;
225      } else if (readSize == 0) {
226        FD_CLR(errFD, &fdToReadSet);  // Hit EOF
227      } else {
228        [mutableErr appendBytes:readBuf length:readSize];
229      }
230    }
231  }
232  // All filehandles closed, wait.
233  [task waitUntilExit];
234
235  NSString *outString = [[[NSString alloc] initWithData:mutableOut
236                                               encoding:NSUTF8StringEncoding]
237                            autorelease];
238  NSString *errString = [[[NSString alloc] initWithData:mutableErr
239                                               encoding:NSUTF8StringEncoding]
240                            autorelease];;
241  if (trimsWhitespace_) {
242    NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
243    outString = [outString stringByTrimmingCharactersInSet:set];
244    if (err) {
245      errString = [errString stringByTrimmingCharactersInSet:set];
246    }
247  }
248
249  // let folks test for nil instead of @""
250  if ([outString length] < 1) {
251    outString = nil;
252  }
253
254  // Handle returning standard error if |err| is not nil
255  if (err) {
256    // let folks test for nil instead of @""
257    if ([errString length] < 1) {
258      *err = nil;
259    } else {
260      *err = errString;
261    }
262  }
263
264  return outString;
265}
266
267- (NSString *)runScript:(NSString *)path {
268  return [self runScript:path withArgs:nil];
269}
270
271- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args {
272  return [self runScript:path withArgs:args standardError:nil];
273}
274
275- (NSString *)runScript:(NSString *)path withArgs:(NSArray *)args standardError:(NSString **)err {
276  if (!path) return nil;
277
278  NSArray *scriptPlusArgs = [[NSArray arrayWithObject:path] arrayByAddingObjectsFromArray:args];
279  NSTask *task = [self interpreterTaskWithAdditionalArgs:scriptPlusArgs];
280  NSFileHandle *fromTask = [[task standardOutput] fileHandleForReading];
281
282  if (!LaunchNSTaskCatchingExceptions(task)) {
283    return nil;
284  }
285
286  NSData *outData = [fromTask readDataToEndOfFile];
287  NSString *output = [[[NSString alloc] initWithData:outData
288                                            encoding:NSUTF8StringEncoding] autorelease];
289
290  // Handle returning standard error if |err| is not nil
291  if (err) {
292    NSFileHandle *stderror = [[task standardError] fileHandleForReading];
293    NSData *errData = [stderror readDataToEndOfFile];
294    *err = [[[NSString alloc] initWithData:errData
295                                  encoding:NSUTF8StringEncoding] autorelease];
296    if (trimsWhitespace_) {
297      *err = [*err stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
298    }
299
300    // let folks test for nil instead of @""
301    if ([*err length] < 1) {
302      *err = nil;
303    }
304  }
305
306  [task terminate];
307
308  if (trimsWhitespace_) {
309    output = [output stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
310  }
311
312  // let folks test for nil instead of @""
313  if ([output length] < 1) {
314    output = nil;
315  }
316
317  return output;
318}
319
320- (NSDictionary *)environment {
321  return environment_;
322}
323
324- (void)setEnvironment:(NSDictionary *)newEnv {
325  [environment_ autorelease];
326  environment_ = [newEnv retain];
327}
328
329- (BOOL)trimsWhitespace {
330  return trimsWhitespace_;
331}
332
333- (void)setTrimsWhitespace:(BOOL)trim {
334  trimsWhitespace_ = trim;
335}
336
337@end
338
339
340@implementation GTMScriptRunner (PrivateMethods)
341
342- (NSTask *)interpreterTaskWithAdditionalArgs:(NSArray *)args {
343  NSTask *task = [[[NSTask alloc] init] autorelease];
344  [task setLaunchPath:interpreter_];
345  [task setStandardInput:[NSPipe pipe]];
346  [task setStandardOutput:[NSPipe pipe]];
347  [task setStandardError:[NSPipe pipe]];
348
349  // If |environment_| is nil, then use an empty dictionary, otherwise use
350  // environment_ exactly.
351  [task setEnvironment:(environment_
352                        ? environment_
353                        : [NSDictionary dictionary])];
354
355  // Build args to interpreter.  The format is:
356  //   interp [args-to-interp] [script-name [args-to-script]]
357  NSArray *allArgs = nil;
358  if (interpreterArgs_) {
359    allArgs = interpreterArgs_;
360  }
361  if (args) {
362    allArgs = allArgs ? [allArgs arrayByAddingObjectsFromArray:args] : args;
363  }
364  if (allArgs){
365    [task setArguments:allArgs];
366  }
367
368  return task;
369}
370
371@end
372
373static BOOL LaunchNSTaskCatchingExceptions(NSTask *task) {
374  BOOL isOK = YES;
375  @try {
376    [task launch];
377  } @catch (id ex) {
378    isOK = NO;
379    _GTMDevLog(@"Failed to launch interpreter '%@' due to: %@",
380               [task launchPath], ex);
381  }
382  return isOK;
383}