jens@20: // jens@20: // MYTask.m jens@20: // Murky jens@20: // jens@20: // Copyright 2008 Jens Alfke. All rights reserved. jens@20: // jens@20: jens@20: #import "MYTask.h" jens@20: jens@20: //FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it. jens@20: jens@20: jens@20: NSString* const MYTaskErrorDomain = @"MYTaskError"; jens@20: NSString* const MYTaskExitCodeKey = @"MYTaskExitCode"; jens@20: NSString* const MYTaskObjectKey = @"MYTask"; jens@20: jens@20: #define MYTaskSynchronousRunLoopMode @"MYTask" jens@20: jens@20: jens@20: @interface MYTask () jens@20: @property (readwrite,nonatomic) BOOL isRunning; jens@20: @property (readwrite,retain,nonatomic) NSError *error; jens@20: - (void) _finishUp; jens@20: @end jens@20: jens@20: jens@20: @implementation MYTask jens@20: jens@20: jens@20: - (id) initWithCommand: (NSString*)command jens@20: arguments: (NSArray*)arguments jens@20: { jens@27: Assert(command); jens@20: self = [super init]; jens@20: if (self != nil) { jens@20: _command = command; jens@20: _arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array]; jens@20: _modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; jens@20: } jens@20: return self; jens@20: } jens@20: jens@20: jens@20: - (id) initWithCommand: (NSString*)command, ... jens@20: { jens@20: NSMutableArray *arguments = [NSMutableArray array]; jens@20: va_list args; jens@20: va_start(args,command); jens@20: id arg; jens@20: while( nil != (arg=va_arg(args,id)) ) jens@20: [arguments addObject: [arg description]]; jens@20: va_end(args); jens@20: jens@20: return [self initWithCommand: command arguments: arguments]; jens@20: } jens@20: jens@20: jens@20: - (id) initWithError: (NSError*)error jens@20: { jens@20: self = [super init]; jens@20: if( self ) { jens@20: _error = error; jens@20: } jens@20: return self; jens@20: } jens@20: jens@20: jens@20: - (NSString*) description jens@20: { jens@20: return [NSString stringWithFormat: @"%@ %@", jens@20: _command, [_arguments componentsJoinedByString: @" "]]; jens@20: } jens@20: jens@20: jens@20: - (void) addArgument: (id)argument jens@20: { jens@20: [_arguments addObject: [argument description]]; jens@20: } jens@20: jens@20: - (void) addArgumentsFromArray: (NSArray*)arguments jens@20: { jens@20: for( id arg in arguments ) jens@20: [_arguments addObject: [arg description]]; jens@20: } jens@20: jens@20: - (void) addArguments: (id)arg, ... jens@20: { jens@20: va_list args; jens@20: va_start(args,arg); jens@20: while( arg ) { jens@20: [_arguments addObject: [arg description]]; jens@20: arg = va_arg(args,id); jens@20: } jens@20: va_end(args); jens@20: } jens@20: jens@20: - (void) prependArguments: (id)arg, ... jens@20: { jens@20: va_list args; jens@20: va_start(args,arg); jens@20: int i=0; jens@20: while( arg ) { jens@20: [_arguments insertObject: [arg description] atIndex: i++]; jens@20: arg = va_arg(args,id); jens@20: } jens@20: va_end(args); jens@20: } jens@20: jens@20: jens@27: - (NSString*) commandLine { jens@27: NSMutableString *desc = [NSMutableString stringWithString: _command]; jens@27: for (NSString *arg in _arguments) { jens@27: [desc appendString: @" "]; jens@27: if ([arg rangeOfString: @" "].length > 0) jens@27: arg = [NSString stringWithFormat: @"'%@'", arg]; jens@27: [desc appendString: arg]; jens@27: } jens@27: return desc; jens@27: } jens@27: jens@27: jens@20: - (void) ignoreOutput jens@20: { jens@20: _ignoreOutput = YES; jens@20: } jens@20: jens@20: jens@20: - (BOOL) makeError: (NSString*)fmt, ... jens@20: { jens@20: va_list args; jens@20: va_start(args,fmt); jens@20: jens@20: NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args]; jens@27: LogTo(MYTask, @"Error: %@",message); jens@20: NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message jens@20: forKey: NSLocalizedDescriptionKey]; jens@20: _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info]; jens@20: jens@20: va_end(args); jens@20: return NO; jens@20: } jens@20: jens@20: jens@20: - (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector jens@20: { jens@20: NSPipe *pipe = [NSPipe pipe]; jens@20: *handle = [pipe fileHandleForReading]; jens@20: [[NSNotificationCenter defaultCenter] addObserver: self selector: selector jens@20: name: NSFileHandleReadCompletionNotification jens@20: object: *handle]; jens@20: [*handle readInBackgroundAndNotifyForModes: _modes]; jens@20: return pipe; jens@20: } jens@20: jens@20: jens@20: - (void) _close jens@20: { jens@20: // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang) jens@20: _outHandle = nil; jens@20: _errHandle = nil; jens@20: [[NSNotificationCenter defaultCenter] removeObserver: self jens@20: name: NSFileHandleReadCompletionNotification jens@20: object: nil]; jens@20: } jens@20: jens@20: jens@20: /** Subclasses can override this. */ jens@20: - (NSTask*) createTask jens@20: { jens@27: Assert(!_task,@"createTask called twice"); jens@20: NSTask *task = [[NSTask alloc] init]; jens@20: task.launchPath = _command; jens@20: task.arguments = _arguments; jens@20: if( _currentDirectoryPath ) jens@20: task.currentDirectoryPath = _currentDirectoryPath; jens@20: return task; jens@20: } jens@20: jens@20: jens@20: - (BOOL) start jens@20: { jens@27: Assert(!_task, @"Task has already been run"); jens@20: if( _error ) jens@20: return NO; jens@20: jens@20: _task = [self createTask]; jens@27: Assert(_task,@"createTask returned nil"); jens@20: jens@27: LogTo(MYTask,@"$ %@", self.commandLine); jens@20: jens@20: _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)]; jens@20: _outputData = [[NSMutableData alloc] init]; jens@20: _task.standardError = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)]; jens@20: _errorData = [[NSMutableData alloc] init]; jens@20: jens@20: [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:) jens@20: name: NSTaskDidTerminateNotification jens@20: object: _task]; jens@20: jens@20: @try{ jens@20: [_task launch]; jens@20: }@catch( id x ) { jens@27: Warn(@"Task failed to launch: %@",x); jens@20: _resultCode = 666; jens@20: [self _close]; jens@20: return [self makeError: @"Exception launching %@: %@",_task.launchPath,x]; jens@20: } jens@20: _taskRunning = YES; jens@20: self.isRunning = YES; jens@20: jens@20: return YES; jens@20: } jens@20: jens@20: jens@20: - (void) stop jens@20: { jens@20: [_task interrupt]; jens@20: [self _close]; jens@20: _taskRunning = NO; jens@20: self.isRunning = NO; jens@20: } jens@20: jens@20: jens@20: - (BOOL) _shouldFinishUp jens@20: { jens@20: return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle)); jens@20: } jens@20: jens@20: jens@20: - (void) _gotOutput: (NSNotification*)n jens@20: { jens@20: NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem]; jens@20: if( n.object == _outHandle ) { jens@20: if( data.length > 0 ) { jens@20: [_outHandle readInBackgroundAndNotifyForModes: _modes]; jens@27: LogTo(HgTaskVerbose, @"Got %u bytes of output",data.length); jens@20: if( _outputData ) { jens@20: [self willChangeValueForKey: @"output"]; jens@20: [self willChangeValueForKey: @"outputData"]; jens@20: [_outputData appendData: data]; jens@20: _output = nil; jens@20: [self didChangeValueForKey: @"outputData"]; jens@20: [self didChangeValueForKey: @"output"]; jens@20: } jens@20: } else { jens@27: LogTo(HgTaskVerbose, @"Closed output"); jens@20: _outHandle = nil; jens@20: if( [self _shouldFinishUp] ) jens@20: [self _finishUp]; jens@20: } jens@20: } jens@20: } jens@20: jens@20: - (void) _gotStderr: (NSNotification*)n jens@20: { jens@20: if( n.object == _errHandle ) { jens@20: NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem]; jens@20: if( data.length > 0 ) { jens@20: [_errHandle readInBackgroundAndNotifyForModes: _modes]; jens@27: LogTo(HgTaskVerbose, @"Got %u bytes of stderr",data.length); jens@20: [self willChangeValueForKey: @"errorData"]; jens@20: [_errorData appendData: data]; jens@20: [self didChangeValueForKey: @"errorData"]; jens@20: } else { jens@27: LogTo(HgTaskVerbose, @"Closed stderr"); jens@20: _errHandle = nil; jens@20: if( [self _shouldFinishUp] ) jens@20: [self _finishUp]; jens@20: } jens@20: } jens@20: } jens@20: jens@20: - (void) _exited: (NSNotification*)n jens@20: { jens@20: _resultCode = _task.terminationStatus; jens@27: LogTo(HgTaskVerbose, @"Exited with result=%i",_resultCode); jens@20: _taskRunning = NO; jens@20: if( [self _shouldFinishUp] ) jens@20: [self _finishUp]; jens@20: else jens@20: [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0]; jens@20: } jens@20: jens@20: jens@20: - (void) _finishUp jens@20: { jens@20: [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil]; jens@20: [self _close]; jens@20: jens@27: LogTo(HgTaskVerbose, @"Finished!"); jens@20: jens@20: if( _resultCode != 0 ) { jens@20: // Handle errors: jens@20: NSString *errStr = nil; jens@20: if( _errorData.length > 0 ) jens@20: errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding]; jens@27: LogTo(MYTask, @" *** task returned %i: %@",_resultCode,errStr); jens@20: if( errStr.length == 0 ) jens@20: errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode]; jens@20: NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent]; jens@20: // For some reason the body text in the alert shown by -presentError: is taken from the jens@20: // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey... jens@20: NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys: jens@20: desc, NSLocalizedDescriptionKey, jens@20: errStr, NSLocalizedRecoverySuggestionErrorKey, jens@20: [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey, jens@20: self, MYTaskObjectKey, jens@20: nil]; jens@20: self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain jens@20: code: kMYTaskError jens@20: userInfo: info]; jens@20: } jens@20: jens@20: [self finished]; jens@20: jens@20: self.isRunning = NO; jens@20: } jens@20: jens@20: - (void) finished jens@20: { jens@20: // This is a hook that subclasses can override to do post-processing. jens@20: } jens@20: jens@20: jens@20: - (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode jens@20: { jens@20: // wait for task to exit: jens@20: while( _task.isRunning || self.isRunning ) jens@20: [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode jens@20: beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]]; jens@20: return (_resultCode==0); jens@20: } jens@20: jens@20: - (BOOL) waitTillFinished jens@20: { jens@20: return [self _waitTillFinishedInMode: _modes.lastObject]; jens@20: } jens@20: jens@20: jens@20: - (BOOL) run jens@20: { jens@20: [_modes addObject: MYTaskSynchronousRunLoopMode]; jens@20: return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode]; jens@20: jens@20: } jens@20: jens@20: jens@20: - (BOOL) run: (NSError**)outError jens@20: { jens@20: BOOL result = [self run]; jens@20: if( outError ) *outError = self.error; jens@20: return result; jens@20: } jens@20: jens@20: jens@20: @synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning; jens@20: jens@20: jens@20: - (NSString*) output jens@20: { jens@20: if( ! _output && _outputData ) { jens@20: _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding]; jens@20: // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1. jens@20: if( ! _output ) { jens@20: _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding]; jens@27: Warn(@"MYTask: Output of '%@' was not valid UTF-8; interpreting as CP1252",self); jens@20: } jens@20: } jens@20: return _output; jens@20: } jens@20: jens@20: - (NSString*) outputAndError jens@20: { jens@20: NSString *result = self.output ?: @""; jens@20: NSString *errorStr = nil; jens@20: if( _error ) jens@20: errorStr = [NSString stringWithFormat: @"%@:\n%@", jens@20: _error.localizedDescription,_error.localizedRecoverySuggestion]; jens@20: else if( _errorData.length > 0 ) jens@20: errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding]; jens@20: if( errorStr ) jens@20: result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result]; jens@20: return result; jens@20: } jens@20: jens@20: + (NSArray*) keyPathsForValuesAffectingOutputAndError jens@20: { jens@20: return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil]; jens@20: } jens@20: jens@20: jens@20: @end