MYTask.m
author Jens Alfke <jens@mooseyard.com>
Wed Jul 01 14:04:56 2009 -0700 (2009-07-01)
changeset 33 d52f6b0d94be
parent 20 5a71993a1a70
child 35 5cab3034d3a1
permissions -rw-r--r--
Added NSData categories for GZip and Mnemonicode.
Minor tweaks elsewhere.
     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     Assert(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 - (NSString*) commandLine {
   111     NSMutableString *desc = [NSMutableString stringWithString: _command];
   112     for (NSString *arg in _arguments) {
   113         [desc appendString: @" "];
   114         if ([arg rangeOfString: @" "].length > 0)
   115             arg = [NSString stringWithFormat: @"'%@'", arg];
   116         [desc appendString: arg];
   117     }
   118     return desc;
   119 }
   120 
   121 
   122 - (void) ignoreOutput
   123 {
   124     _ignoreOutput = YES;
   125 }
   126 
   127 
   128 - (BOOL) makeError: (NSString*)fmt, ...
   129 {
   130     va_list args;
   131     va_start(args,fmt);
   132 
   133     NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
   134     LogTo(MYTask, @"Error: %@",message);
   135     NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
   136                                                                    forKey: NSLocalizedDescriptionKey];
   137     _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
   138 
   139     va_end(args);
   140     return NO;
   141 }
   142 
   143 
   144 - (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
   145 {
   146     NSPipe *pipe = [NSPipe pipe];
   147     *handle = [pipe fileHandleForReading];
   148     [[NSNotificationCenter defaultCenter] addObserver: self selector: selector
   149                                                  name: NSFileHandleReadCompletionNotification
   150                                                object: *handle];
   151     [*handle readInBackgroundAndNotifyForModes: _modes];
   152     return pipe;
   153 }
   154 
   155 
   156 - (void) _close
   157 {
   158     // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
   159     _outHandle = nil;
   160     _errHandle = nil;
   161     [[NSNotificationCenter defaultCenter] removeObserver: self 
   162                                                     name: NSFileHandleReadCompletionNotification
   163                                                   object: nil];
   164 }
   165 
   166 
   167 /** Subclasses can override this. */
   168 - (NSTask*) createTask
   169 {
   170     Assert(!_task,@"createTask called twice");
   171     NSTask *task = [[NSTask alloc] init];
   172     task.launchPath = _command;
   173     task.arguments = _arguments;
   174     if( _currentDirectoryPath )
   175         task.currentDirectoryPath = _currentDirectoryPath;
   176     return task;
   177 }    
   178 
   179 
   180 - (BOOL) start
   181 {
   182     Assert(!_task, @"Task has already been run");
   183     if( _error )
   184         return NO;
   185     
   186     _task = [self createTask];
   187     Assert(_task,@"createTask returned nil");
   188     
   189     LogTo(MYTask,@"$ %@", self.commandLine);
   190     
   191     _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
   192     _outputData =  [[NSMutableData alloc] init];
   193     _task.standardError  = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
   194     _errorData =  [[NSMutableData alloc] init];
   195     
   196     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
   197                                                  name: NSTaskDidTerminateNotification
   198                                                object: _task];
   199     
   200     @try{
   201         [_task launch];
   202     }@catch( id x ) {
   203         Warn(@"Task failed to launch: %@",x);
   204         _resultCode = 666;
   205         [self _close];
   206         return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
   207     }
   208     _taskRunning = YES;
   209     self.isRunning = YES;
   210     
   211     return YES;
   212 }
   213 
   214 
   215 - (void) stop
   216 {
   217     [_task interrupt];
   218     [self _close];
   219     _taskRunning = NO;
   220     self.isRunning = NO;
   221 }
   222 
   223 
   224 - (BOOL) _shouldFinishUp
   225 {
   226     return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
   227 }
   228 
   229 
   230 - (void) _gotOutput: (NSNotification*)n
   231 {
   232     NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   233     if( n.object == _outHandle ) {
   234         if( data.length > 0 ) {
   235             [_outHandle readInBackgroundAndNotifyForModes: _modes];
   236             LogTo(HgTaskVerbose, @"Got %u bytes of output",data.length);
   237             if( _outputData ) {
   238                 [self willChangeValueForKey: @"output"];
   239                 [self willChangeValueForKey: @"outputData"];
   240                 [_outputData appendData: data];
   241                 _output = nil;
   242                 [self didChangeValueForKey: @"outputData"];
   243                 [self didChangeValueForKey: @"output"];
   244             }
   245         } else {
   246             LogTo(HgTaskVerbose, @"Closed output");
   247             _outHandle = nil;
   248             if( [self _shouldFinishUp] )
   249                 [self _finishUp];
   250         }
   251     }
   252 }
   253 
   254 - (void) _gotStderr: (NSNotification*)n
   255 {
   256     if( n.object == _errHandle ) {
   257         NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   258         if( data.length > 0 ) {
   259             [_errHandle readInBackgroundAndNotifyForModes: _modes];
   260             LogTo(HgTaskVerbose, @"Got %u bytes of stderr",data.length);
   261             [self willChangeValueForKey: @"errorData"];
   262             [_errorData appendData: data];
   263             [self didChangeValueForKey: @"errorData"];
   264         } else {
   265             LogTo(HgTaskVerbose, @"Closed stderr");
   266             _errHandle = nil;
   267             if( [self _shouldFinishUp] )
   268                 [self _finishUp];
   269         }
   270     }
   271 }
   272 
   273 - (void) _exited: (NSNotification*)n
   274 {
   275     _resultCode = _task.terminationStatus;
   276     LogTo(HgTaskVerbose, @"Exited with result=%i",_resultCode);
   277     _taskRunning = NO;
   278     if( [self _shouldFinishUp] )
   279         [self _finishUp];
   280     else
   281         [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
   282 }
   283 
   284 
   285 - (void) _finishUp
   286 {
   287     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
   288     [self _close];
   289 
   290     LogTo(HgTaskVerbose, @"Finished!");
   291 
   292     if( _resultCode != 0 ) {
   293         // Handle errors:
   294         NSString *errStr = nil;
   295         if( _errorData.length > 0 )
   296             errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   297         LogTo(MYTask, @"    *** task returned %i: %@",_resultCode,errStr);
   298         if( errStr.length == 0 )
   299             errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
   300         NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
   301         // For some reason the body text in the alert shown by -presentError: is taken from the
   302         // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
   303         NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
   304                                      desc,                                  NSLocalizedDescriptionKey,
   305                                      errStr,                                NSLocalizedRecoverySuggestionErrorKey,
   306                                      [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
   307                                      self,                                  MYTaskObjectKey,
   308                                      nil];
   309         self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain 
   310                                                 code: kMYTaskError
   311                                             userInfo: info];
   312     }
   313 
   314     [self finished];
   315 
   316     self.isRunning = NO;
   317 }
   318 
   319 - (void) finished
   320 {
   321     // This is a hook that subclasses can override to do post-processing.
   322 }
   323 
   324 
   325 - (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
   326 {
   327     // wait for task to exit:
   328     while( _task.isRunning || self.isRunning )
   329         [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
   330                                  beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
   331     return (_resultCode==0);
   332 }
   333 
   334 - (BOOL) waitTillFinished
   335 {
   336     return [self _waitTillFinishedInMode: _modes.lastObject];
   337 }
   338 
   339 
   340 - (BOOL) run
   341 {
   342     [_modes addObject: MYTaskSynchronousRunLoopMode];
   343     return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
   344     
   345 }    
   346 
   347 
   348 - (BOOL) run: (NSError**)outError
   349 {
   350     BOOL result = [self run];
   351     if( outError ) *outError = self.error;
   352     return result;
   353 }
   354 
   355 
   356 @synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
   357 
   358 
   359 - (NSString*) output
   360 {
   361     if( ! _output && _outputData ) {
   362         _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
   363         // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
   364         if( ! _output ) {
   365             _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
   366             Warn(@"MYTask: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
   367         }
   368     }
   369     return _output;
   370 }
   371 
   372 - (NSString*) outputAndError
   373 {
   374     NSString *result = self.output ?: @"";
   375     NSString *errorStr = nil;
   376     if( _error )
   377         errorStr = [NSString stringWithFormat: @"%@:\n%@",
   378                     _error.localizedDescription,_error.localizedRecoverySuggestion];
   379     else if( _errorData.length > 0 )
   380         errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   381     if( errorStr )
   382         result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
   383     return result;
   384 }
   385 
   386 + (NSArray*) keyPathsForValuesAffectingOutputAndError
   387 {
   388     return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];
   389 }
   390 
   391 
   392 @end