MYTask.m
changeset 20 5a71993a1a70
child 27 256370e8935a
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/MYTask.m	Sat Mar 28 09:36:46 2009 -0700
     1.3 @@ -0,0 +1,380 @@
     1.4 +//
     1.5 +//  MYTask.m
     1.6 +//  Murky
     1.7 +//
     1.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     1.9 +//
    1.10 +
    1.11 +#import "MYTask.h"
    1.12 +
    1.13 +//FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it.
    1.14 +
    1.15 +
    1.16 +NSString* const MYTaskErrorDomain = @"MYTaskError";
    1.17 +NSString* const MYTaskExitCodeKey = @"MYTaskExitCode";
    1.18 +NSString* const MYTaskObjectKey = @"MYTask";
    1.19 +
    1.20 +#define MYTaskSynchronousRunLoopMode @"MYTask"
    1.21 +
    1.22 +
    1.23 +@interface MYTask ()
    1.24 +@property (readwrite,nonatomic) BOOL isRunning;
    1.25 +@property (readwrite,retain,nonatomic) NSError *error;
    1.26 +- (void) _finishUp;
    1.27 +@end
    1.28 +
    1.29 +
    1.30 +@implementation MYTask
    1.31 +
    1.32 +
    1.33 +- (id) initWithCommand: (NSString*)command
    1.34 +             arguments: (NSArray*)arguments
    1.35 +{
    1.36 +    NSParameterAssert(command);
    1.37 +    self = [super init];
    1.38 +    if (self != nil) {
    1.39 +        _command = command;
    1.40 +        _arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array];
    1.41 +        _modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil];
    1.42 +    }
    1.43 +    return self;
    1.44 +}
    1.45 +
    1.46 +
    1.47 +- (id) initWithCommand: (NSString*)command, ...
    1.48 +{
    1.49 +    NSMutableArray *arguments = [NSMutableArray array];
    1.50 +    va_list args;
    1.51 +    va_start(args,command);
    1.52 +    id arg;
    1.53 +    while( nil != (arg=va_arg(args,id)) )
    1.54 +        [arguments addObject: [arg description]];
    1.55 +    va_end(args);
    1.56 +    
    1.57 +    return [self initWithCommand: command arguments: arguments];
    1.58 +}
    1.59 +
    1.60 +
    1.61 +- (id) initWithError: (NSError*)error
    1.62 +{
    1.63 +    self = [super init];
    1.64 +    if( self ) {
    1.65 +        _error = error;
    1.66 +    }
    1.67 +    return self;
    1.68 +}
    1.69 +
    1.70 +
    1.71 +- (NSString*) description
    1.72 +{
    1.73 +    return [NSString stringWithFormat: @"%@ %@", 
    1.74 +            _command, [_arguments componentsJoinedByString: @" "]];
    1.75 +}
    1.76 +
    1.77 +
    1.78 +- (void) addArgument: (id)argument
    1.79 +{
    1.80 +    [_arguments addObject: [argument description]];
    1.81 +}
    1.82 +
    1.83 +- (void) addArgumentsFromArray: (NSArray*)arguments
    1.84 +{
    1.85 +    for( id arg in arguments )
    1.86 +        [_arguments addObject: [arg description]];
    1.87 +}
    1.88 +
    1.89 +- (void) addArguments: (id)arg, ...
    1.90 +{
    1.91 +    va_list args;
    1.92 +    va_start(args,arg);
    1.93 +    while( arg ) {
    1.94 +        [_arguments addObject: [arg description]];
    1.95 +        arg = va_arg(args,id);
    1.96 +    }
    1.97 +    va_end(args);
    1.98 +}
    1.99 +
   1.100 +- (void) prependArguments: (id)arg, ...
   1.101 +{
   1.102 +    va_list args;
   1.103 +    va_start(args,arg);
   1.104 +    int i=0;
   1.105 +    while( arg ) {
   1.106 +        [_arguments insertObject: [arg description] atIndex: i++];
   1.107 +        arg = va_arg(args,id);
   1.108 +    }
   1.109 +    va_end(args);
   1.110 +}
   1.111 +
   1.112 +
   1.113 +- (void) ignoreOutput
   1.114 +{
   1.115 +    _ignoreOutput = YES;
   1.116 +}
   1.117 +
   1.118 +
   1.119 +- (BOOL) makeError: (NSString*)fmt, ...
   1.120 +{
   1.121 +    va_list args;
   1.122 +    va_start(args,fmt);
   1.123 +
   1.124 +    NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
   1.125 +    Log(@"MYTask Error: %@",message);
   1.126 +    NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
   1.127 +                                                                   forKey: NSLocalizedDescriptionKey];
   1.128 +    _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
   1.129 +
   1.130 +    va_end(args);
   1.131 +    return NO;
   1.132 +}
   1.133 +
   1.134 +
   1.135 +- (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
   1.136 +{
   1.137 +    NSPipe *pipe = [NSPipe pipe];
   1.138 +    *handle = [pipe fileHandleForReading];
   1.139 +    [[NSNotificationCenter defaultCenter] addObserver: self selector: selector
   1.140 +                                                 name: NSFileHandleReadCompletionNotification
   1.141 +                                               object: *handle];
   1.142 +    [*handle readInBackgroundAndNotifyForModes: _modes];
   1.143 +    return pipe;
   1.144 +}
   1.145 +
   1.146 +
   1.147 +- (void) _close
   1.148 +{
   1.149 +    // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
   1.150 +    _outHandle = nil;
   1.151 +    _errHandle = nil;
   1.152 +    [[NSNotificationCenter defaultCenter] removeObserver: self 
   1.153 +                                                    name: NSFileHandleReadCompletionNotification
   1.154 +                                                  object: nil];
   1.155 +}
   1.156 +
   1.157 +
   1.158 +/** Subclasses can override this. */
   1.159 +- (NSTask*) createTask
   1.160 +{
   1.161 +    NSAssert(!_task,@"createTask called twice");
   1.162 +    NSTask *task = [[NSTask alloc] init];
   1.163 +    task.launchPath = _command;
   1.164 +    task.arguments = _arguments;
   1.165 +    if( _currentDirectoryPath )
   1.166 +        task.currentDirectoryPath = _currentDirectoryPath;
   1.167 +    return task;
   1.168 +}    
   1.169 +
   1.170 +
   1.171 +- (BOOL) start
   1.172 +{
   1.173 +    NSAssert(!_task, @"Task has already been run");
   1.174 +    if( _error )
   1.175 +        return NO;
   1.176 +    
   1.177 +    _task = [self createTask];
   1.178 +    NSAssert(_task,@"createTask returned nil");
   1.179 +    
   1.180 +    Log(@"Task: %@ %@",_task.launchPath,[_task.arguments componentsJoinedByString: @" "]);
   1.181 +    
   1.182 +    _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
   1.183 +    _outputData =  [[NSMutableData alloc] init];
   1.184 +    _task.standardError  = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
   1.185 +    _errorData =  [[NSMutableData alloc] init];
   1.186 +    
   1.187 +    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
   1.188 +                                                 name: NSTaskDidTerminateNotification
   1.189 +                                               object: _task];
   1.190 +    
   1.191 +    @try{
   1.192 +        [_task launch];
   1.193 +    }@catch( id x ) {
   1.194 +        Log(@"Task failed to launch: %@",x);
   1.195 +        _resultCode = 666;
   1.196 +        [self _close];
   1.197 +        return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
   1.198 +    }
   1.199 +    _taskRunning = YES;
   1.200 +    self.isRunning = YES;
   1.201 +    
   1.202 +    return YES;
   1.203 +}
   1.204 +
   1.205 +
   1.206 +- (void) stop
   1.207 +{
   1.208 +    [_task interrupt];
   1.209 +    [self _close];
   1.210 +    _taskRunning = NO;
   1.211 +    self.isRunning = NO;
   1.212 +}
   1.213 +
   1.214 +
   1.215 +- (BOOL) _shouldFinishUp
   1.216 +{
   1.217 +    return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
   1.218 +}
   1.219 +
   1.220 +
   1.221 +- (void) _gotOutput: (NSNotification*)n
   1.222 +{
   1.223 +    NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   1.224 +    if( n.object == _outHandle ) {
   1.225 +        if( data.length > 0 ) {
   1.226 +            [_outHandle readInBackgroundAndNotifyForModes: _modes];
   1.227 +            LogTo(Task,@"Got %u bytes of output",data.length);
   1.228 +            if( _outputData ) {
   1.229 +                [self willChangeValueForKey: @"output"];
   1.230 +                [self willChangeValueForKey: @"outputData"];
   1.231 +                [_outputData appendData: data];
   1.232 +                _output = nil;
   1.233 +                [self didChangeValueForKey: @"outputData"];
   1.234 +                [self didChangeValueForKey: @"output"];
   1.235 +            }
   1.236 +        } else {
   1.237 +            LogTo(Task,@"Closed output");
   1.238 +            _outHandle = nil;
   1.239 +            if( [self _shouldFinishUp] )
   1.240 +                [self _finishUp];
   1.241 +        }
   1.242 +    }
   1.243 +}
   1.244 +
   1.245 +- (void) _gotStderr: (NSNotification*)n
   1.246 +{
   1.247 +    if( n.object == _errHandle ) {
   1.248 +        NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   1.249 +        if( data.length > 0 ) {
   1.250 +            [_errHandle readInBackgroundAndNotifyForModes: _modes];
   1.251 +            LogTo(Task,@"Got %u bytes of stderr",data.length);
   1.252 +            [self willChangeValueForKey: @"errorData"];
   1.253 +            [_errorData appendData: data];
   1.254 +            [self didChangeValueForKey: @"errorData"];
   1.255 +        } else {
   1.256 +            LogTo(Task,@"Closed stderr");
   1.257 +            _errHandle = nil;
   1.258 +            if( [self _shouldFinishUp] )
   1.259 +                [self _finishUp];
   1.260 +        }
   1.261 +    }
   1.262 +}
   1.263 +
   1.264 +- (void) _exited: (NSNotification*)n
   1.265 +{
   1.266 +    _resultCode = _task.terminationStatus;
   1.267 +    LogTo(Task,@"Exited with result=%i",_resultCode);
   1.268 +    _taskRunning = NO;
   1.269 +    if( [self _shouldFinishUp] )
   1.270 +        [self _finishUp];
   1.271 +    else
   1.272 +        [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
   1.273 +}
   1.274 +
   1.275 +
   1.276 +- (void) _finishUp
   1.277 +{
   1.278 +    [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
   1.279 +    [self _close];
   1.280 +
   1.281 +    LogTo(Task,@"Finished!");
   1.282 +
   1.283 +    if( _resultCode != 0 ) {
   1.284 +        // Handle errors:
   1.285 +        NSString *errStr = nil;
   1.286 +        if( _errorData.length > 0 )
   1.287 +            errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   1.288 +        Log(@"    *** task returned %i: %@",_resultCode,errStr);
   1.289 +        if( errStr.length == 0 )
   1.290 +            errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
   1.291 +        NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
   1.292 +        // For some reason the body text in the alert shown by -presentError: is taken from the
   1.293 +        // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
   1.294 +        NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
   1.295 +                                     desc,                                  NSLocalizedDescriptionKey,
   1.296 +                                     errStr,                                NSLocalizedRecoverySuggestionErrorKey,
   1.297 +                                     [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
   1.298 +                                     self,                                  MYTaskObjectKey,
   1.299 +                                     nil];
   1.300 +        self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain 
   1.301 +                                                code: kMYTaskError
   1.302 +                                            userInfo: info];
   1.303 +    }
   1.304 +
   1.305 +    [self finished];
   1.306 +
   1.307 +    self.isRunning = NO;
   1.308 +}
   1.309 +
   1.310 +- (void) finished
   1.311 +{
   1.312 +    // This is a hook that subclasses can override to do post-processing.
   1.313 +}
   1.314 +
   1.315 +
   1.316 +- (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
   1.317 +{
   1.318 +    // wait for task to exit:
   1.319 +    while( _task.isRunning || self.isRunning )
   1.320 +        [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
   1.321 +                                 beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
   1.322 +    return (_resultCode==0);
   1.323 +}
   1.324 +
   1.325 +- (BOOL) waitTillFinished
   1.326 +{
   1.327 +    return [self _waitTillFinishedInMode: _modes.lastObject];
   1.328 +}
   1.329 +
   1.330 +
   1.331 +- (BOOL) run
   1.332 +{
   1.333 +    [_modes addObject: MYTaskSynchronousRunLoopMode];
   1.334 +    return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
   1.335 +    
   1.336 +}    
   1.337 +
   1.338 +
   1.339 +- (BOOL) run: (NSError**)outError
   1.340 +{
   1.341 +    BOOL result = [self run];
   1.342 +    if( outError ) *outError = self.error;
   1.343 +    return result;
   1.344 +}
   1.345 +
   1.346 +
   1.347 +@synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
   1.348 +
   1.349 +
   1.350 +- (NSString*) output
   1.351 +{
   1.352 +    if( ! _output && _outputData ) {
   1.353 +        _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
   1.354 +        // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
   1.355 +        if( ! _output ) {
   1.356 +            _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
   1.357 +            Log(@"Warning: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
   1.358 +        }
   1.359 +    }
   1.360 +    return _output;
   1.361 +}
   1.362 +
   1.363 +- (NSString*) outputAndError
   1.364 +{
   1.365 +    NSString *result = self.output ?: @"";
   1.366 +    NSString *errorStr = nil;
   1.367 +    if( _error )
   1.368 +        errorStr = [NSString stringWithFormat: @"%@:\n%@",
   1.369 +                    _error.localizedDescription,_error.localizedRecoverySuggestion];
   1.370 +    else if( _errorData.length > 0 )
   1.371 +        errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   1.372 +    if( errorStr )
   1.373 +        result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
   1.374 +    return result;
   1.375 +}
   1.376 +
   1.377 ++ (NSArray*) keyPathsForValuesAffectingOutputAndError
   1.378 +{
   1.379 +    return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];
   1.380 +}
   1.381 +
   1.382 +
   1.383 +@end