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