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