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