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