MYTask.m
author Jens Alfke <jens@mooseyard.com>
Sat Mar 28 09:36:46 2009 -0700 (2009-03-28)
changeset 20 5a71993a1a70
child 27 256370e8935a
permissions -rw-r--r--
Added some new utilities, taken from Murky.
     1 //
     2 //  MYTask.m
     3 //  Murky
     4 //
     5 //  Copyright 2008 Jens Alfke. All rights reserved.
     6 //
     7 
     8 #import "MYTask.h"
     9 
    10 //FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it.
    11 
    12 
    13 NSString* const MYTaskErrorDomain = @"MYTaskError";
    14 NSString* const MYTaskExitCodeKey = @"MYTaskExitCode";
    15 NSString* const MYTaskObjectKey = @"MYTask";
    16 
    17 #define MYTaskSynchronousRunLoopMode @"MYTask"
    18 
    19 
    20 @interface MYTask ()
    21 @property (readwrite,nonatomic) BOOL isRunning;
    22 @property (readwrite,retain,nonatomic) NSError *error;
    23 - (void) _finishUp;
    24 @end
    25 
    26 
    27 @implementation MYTask
    28 
    29 
    30 - (id) initWithCommand: (NSString*)command
    31              arguments: (NSArray*)arguments
    32 {
    33     NSParameterAssert(command);
    34     self = [super init];
    35     if (self != nil) {
    36         _command = command;
    37         _arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array];
    38         _modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil];
    39     }
    40     return self;
    41 }
    42 
    43 
    44 - (id) initWithCommand: (NSString*)command, ...
    45 {
    46     NSMutableArray *arguments = [NSMutableArray array];
    47     va_list args;
    48     va_start(args,command);
    49     id arg;
    50     while( nil != (arg=va_arg(args,id)) )
    51         [arguments addObject: [arg description]];
    52     va_end(args);
    53     
    54     return [self initWithCommand: command arguments: arguments];
    55 }
    56 
    57 
    58 - (id) initWithError: (NSError*)error
    59 {
    60     self = [super init];
    61     if( self ) {
    62         _error = error;
    63     }
    64     return self;
    65 }
    66 
    67 
    68 - (NSString*) description
    69 {
    70     return [NSString stringWithFormat: @"%@ %@", 
    71             _command, [_arguments componentsJoinedByString: @" "]];
    72 }
    73 
    74 
    75 - (void) addArgument: (id)argument
    76 {
    77     [_arguments addObject: [argument description]];
    78 }
    79 
    80 - (void) addArgumentsFromArray: (NSArray*)arguments
    81 {
    82     for( id arg in arguments )
    83         [_arguments addObject: [arg description]];
    84 }
    85 
    86 - (void) addArguments: (id)arg, ...
    87 {
    88     va_list args;
    89     va_start(args,arg);
    90     while( arg ) {
    91         [_arguments addObject: [arg description]];
    92         arg = va_arg(args,id);
    93     }
    94     va_end(args);
    95 }
    96 
    97 - (void) prependArguments: (id)arg, ...
    98 {
    99     va_list args;
   100     va_start(args,arg);
   101     int i=0;
   102     while( arg ) {
   103         [_arguments insertObject: [arg description] atIndex: i++];
   104         arg = va_arg(args,id);
   105     }
   106     va_end(args);
   107 }
   108 
   109 
   110 - (void) ignoreOutput
   111 {
   112     _ignoreOutput = YES;
   113 }
   114 
   115 
   116 - (BOOL) makeError: (NSString*)fmt, ...
   117 {
   118     va_list args;
   119     va_start(args,fmt);
   120 
   121     NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
   122     Log(@"MYTask Error: %@",message);
   123     NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
   124                                                                    forKey: NSLocalizedDescriptionKey];
   125     _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
   126 
   127     va_end(args);
   128     return NO;
   129 }
   130 
   131 
   132 - (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
   133 {
   134     NSPipe *pipe = [NSPipe pipe];
   135     *handle = [pipe fileHandleForReading];
   136     [[NSNotificationCenter defaultCenter] addObserver: self selector: selector
   137                                                  name: NSFileHandleReadCompletionNotification
   138                                                object: *handle];
   139     [*handle readInBackgroundAndNotifyForModes: _modes];
   140     return pipe;
   141 }
   142 
   143 
   144 - (void) _close
   145 {
   146     // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
   147     _outHandle = nil;
   148     _errHandle = nil;
   149     [[NSNotificationCenter defaultCenter] removeObserver: self 
   150                                                     name: NSFileHandleReadCompletionNotification
   151                                                   object: nil];
   152 }
   153 
   154 
   155 /** Subclasses can override this. */
   156 - (NSTask*) createTask
   157 {
   158     NSAssert(!_task,@"createTask called twice");
   159     NSTask *task = [[NSTask alloc] init];
   160     task.launchPath = _command;
   161     task.arguments = _arguments;
   162     if( _currentDirectoryPath )
   163         task.currentDirectoryPath = _currentDirectoryPath;
   164     return task;
   165 }    
   166 
   167 
   168 - (BOOL) start
   169 {
   170     NSAssert(!_task, @"Task has already been run");
   171     if( _error )
   172         return NO;
   173     
   174     _task = [self createTask];
   175     NSAssert(_task,@"createTask returned nil");
   176     
   177     Log(@"Task: %@ %@",_task.launchPath,[_task.arguments componentsJoinedByString: @" "]);
   178     
   179     _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
   180     _outputData =  [[NSMutableData alloc] init];
   181     _task.standardError  = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
   182     _errorData =  [[NSMutableData alloc] init];
   183     
   184     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
   185                                                  name: NSTaskDidTerminateNotification
   186                                                object: _task];
   187     
   188     @try{
   189         [_task launch];
   190     }@catch( id x ) {
   191         Log(@"Task failed to launch: %@",x);
   192         _resultCode = 666;
   193         [self _close];
   194         return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
   195     }
   196     _taskRunning = YES;
   197     self.isRunning = YES;
   198     
   199     return YES;
   200 }
   201 
   202 
   203 - (void) stop
   204 {
   205     [_task interrupt];
   206     [self _close];
   207     _taskRunning = NO;
   208     self.isRunning = NO;
   209 }
   210 
   211 
   212 - (BOOL) _shouldFinishUp
   213 {
   214     return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
   215 }
   216 
   217 
   218 - (void) _gotOutput: (NSNotification*)n
   219 {
   220     NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   221     if( n.object == _outHandle ) {
   222         if( data.length > 0 ) {
   223             [_outHandle readInBackgroundAndNotifyForModes: _modes];
   224             LogTo(Task,@"Got %u bytes of output",data.length);
   225             if( _outputData ) {
   226                 [self willChangeValueForKey: @"output"];
   227                 [self willChangeValueForKey: @"outputData"];
   228                 [_outputData appendData: data];
   229                 _output = nil;
   230                 [self didChangeValueForKey: @"outputData"];
   231                 [self didChangeValueForKey: @"output"];
   232             }
   233         } else {
   234             LogTo(Task,@"Closed output");
   235             _outHandle = nil;
   236             if( [self _shouldFinishUp] )
   237                 [self _finishUp];
   238         }
   239     }
   240 }
   241 
   242 - (void) _gotStderr: (NSNotification*)n
   243 {
   244     if( n.object == _errHandle ) {
   245         NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   246         if( data.length > 0 ) {
   247             [_errHandle readInBackgroundAndNotifyForModes: _modes];
   248             LogTo(Task,@"Got %u bytes of stderr",data.length);
   249             [self willChangeValueForKey: @"errorData"];
   250             [_errorData appendData: data];
   251             [self didChangeValueForKey: @"errorData"];
   252         } else {
   253             LogTo(Task,@"Closed stderr");
   254             _errHandle = nil;
   255             if( [self _shouldFinishUp] )
   256                 [self _finishUp];
   257         }
   258     }
   259 }
   260 
   261 - (void) _exited: (NSNotification*)n
   262 {
   263     _resultCode = _task.terminationStatus;
   264     LogTo(Task,@"Exited with result=%i",_resultCode);
   265     _taskRunning = NO;
   266     if( [self _shouldFinishUp] )
   267         [self _finishUp];
   268     else
   269         [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
   270 }
   271 
   272 
   273 - (void) _finishUp
   274 {
   275     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
   276     [self _close];
   277 
   278     LogTo(Task,@"Finished!");
   279 
   280     if( _resultCode != 0 ) {
   281         // Handle errors:
   282         NSString *errStr = nil;
   283         if( _errorData.length > 0 )
   284             errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   285         Log(@"    *** task returned %i: %@",_resultCode,errStr);
   286         if( errStr.length == 0 )
   287             errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
   288         NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
   289         // For some reason the body text in the alert shown by -presentError: is taken from the
   290         // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
   291         NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
   292                                      desc,                                  NSLocalizedDescriptionKey,
   293                                      errStr,                                NSLocalizedRecoverySuggestionErrorKey,
   294                                      [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
   295                                      self,                                  MYTaskObjectKey,
   296                                      nil];
   297         self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain 
   298                                                 code: kMYTaskError
   299                                             userInfo: info];
   300     }
   301 
   302     [self finished];
   303 
   304     self.isRunning = NO;
   305 }
   306 
   307 - (void) finished
   308 {
   309     // This is a hook that subclasses can override to do post-processing.
   310 }
   311 
   312 
   313 - (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
   314 {
   315     // wait for task to exit:
   316     while( _task.isRunning || self.isRunning )
   317         [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
   318                                  beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
   319     return (_resultCode==0);
   320 }
   321 
   322 - (BOOL) waitTillFinished
   323 {
   324     return [self _waitTillFinishedInMode: _modes.lastObject];
   325 }
   326 
   327 
   328 - (BOOL) run
   329 {
   330     [_modes addObject: MYTaskSynchronousRunLoopMode];
   331     return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
   332     
   333 }    
   334 
   335 
   336 - (BOOL) run: (NSError**)outError
   337 {
   338     BOOL result = [self run];
   339     if( outError ) *outError = self.error;
   340     return result;
   341 }
   342 
   343 
   344 @synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
   345 
   346 
   347 - (NSString*) output
   348 {
   349     if( ! _output && _outputData ) {
   350         _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
   351         // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
   352         if( ! _output ) {
   353             _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
   354             Log(@"Warning: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
   355         }
   356     }
   357     return _output;
   358 }
   359 
   360 - (NSString*) outputAndError
   361 {
   362     NSString *result = self.output ?: @"";
   363     NSString *errorStr = nil;
   364     if( _error )
   365         errorStr = [NSString stringWithFormat: @"%@:\n%@",
   366                     _error.localizedDescription,_error.localizedRecoverySuggestion];
   367     else if( _errorData.length > 0 )
   368         errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   369     if( errorStr )
   370         result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
   371     return result;
   372 }
   373 
   374 + (NSArray*) keyPathsForValuesAffectingOutputAndError
   375 {
   376     return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];
   377 }
   378 
   379 
   380 @end