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