Added some new utilities, taken from Murky.
5 // Copyright 2008 Jens Alfke. All rights reserved.
10 //FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it.
13 NSString* const MYTaskErrorDomain = @"MYTaskError";
14 NSString* const MYTaskExitCodeKey = @"MYTaskExitCode";
15 NSString* const MYTaskObjectKey = @"MYTask";
17 #define MYTaskSynchronousRunLoopMode @"MYTask"
21 @property (readwrite,nonatomic) BOOL isRunning;
22 @property (readwrite,retain,nonatomic) NSError *error;
27 @implementation MYTask
30 - (id) initWithCommand: (NSString*)command
31 arguments: (NSArray*)arguments
33 NSParameterAssert(command);
37 _arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array];
38 _modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil];
44 - (id) initWithCommand: (NSString*)command, ...
46 NSMutableArray *arguments = [NSMutableArray array];
48 va_start(args,command);
50 while( nil != (arg=va_arg(args,id)) )
51 [arguments addObject: [arg description]];
54 return [self initWithCommand: command arguments: arguments];
58 - (id) initWithError: (NSError*)error
68 - (NSString*) description
70 return [NSString stringWithFormat: @"%@ %@",
71 _command, [_arguments componentsJoinedByString: @" "]];
75 - (void) addArgument: (id)argument
77 [_arguments addObject: [argument description]];
80 - (void) addArgumentsFromArray: (NSArray*)arguments
82 for( id arg in arguments )
83 [_arguments addObject: [arg description]];
86 - (void) addArguments: (id)arg, ...
91 [_arguments addObject: [arg description]];
92 arg = va_arg(args,id);
97 - (void) prependArguments: (id)arg, ...
103 [_arguments insertObject: [arg description] atIndex: i++];
104 arg = va_arg(args,id);
110 - (void) ignoreOutput
116 - (BOOL) makeError: (NSString*)fmt, ...
121 NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
122 Log(@"MYTask Error: %@",message);
123 NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
124 forKey: NSLocalizedDescriptionKey];
125 _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
132 - (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
134 NSPipe *pipe = [NSPipe pipe];
135 *handle = [pipe fileHandleForReading];
136 [[NSNotificationCenter defaultCenter] addObserver: self selector: selector
137 name: NSFileHandleReadCompletionNotification
139 [*handle readInBackgroundAndNotifyForModes: _modes];
146 // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
149 [[NSNotificationCenter defaultCenter] removeObserver: self
150 name: NSFileHandleReadCompletionNotification
155 /** Subclasses can override this. */
156 - (NSTask*) createTask
158 NSAssert(!_task,@"createTask called twice");
159 NSTask *task = [[NSTask alloc] init];
160 task.launchPath = _command;
161 task.arguments = _arguments;
162 if( _currentDirectoryPath )
163 task.currentDirectoryPath = _currentDirectoryPath;
170 NSAssert(!_task, @"Task has already been run");
174 _task = [self createTask];
175 NSAssert(_task,@"createTask returned nil");
177 Log(@"Task: %@ %@",_task.launchPath,[_task.arguments componentsJoinedByString: @" "]);
179 _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
180 _outputData = [[NSMutableData alloc] init];
181 _task.standardError = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
182 _errorData = [[NSMutableData alloc] init];
184 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
185 name: NSTaskDidTerminateNotification
191 Log(@"Task failed to launch: %@",x);
194 return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
197 self.isRunning = YES;
212 - (BOOL) _shouldFinishUp
214 return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
218 - (void) _gotOutput: (NSNotification*)n
220 NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
221 if( n.object == _outHandle ) {
222 if( data.length > 0 ) {
223 [_outHandle readInBackgroundAndNotifyForModes: _modes];
224 LogTo(Task,@"Got %u bytes of output",data.length);
226 [self willChangeValueForKey: @"output"];
227 [self willChangeValueForKey: @"outputData"];
228 [_outputData appendData: data];
230 [self didChangeValueForKey: @"outputData"];
231 [self didChangeValueForKey: @"output"];
234 LogTo(Task,@"Closed output");
236 if( [self _shouldFinishUp] )
242 - (void) _gotStderr: (NSNotification*)n
244 if( n.object == _errHandle ) {
245 NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
246 if( data.length > 0 ) {
247 [_errHandle readInBackgroundAndNotifyForModes: _modes];
248 LogTo(Task,@"Got %u bytes of stderr",data.length);
249 [self willChangeValueForKey: @"errorData"];
250 [_errorData appendData: data];
251 [self didChangeValueForKey: @"errorData"];
253 LogTo(Task,@"Closed stderr");
255 if( [self _shouldFinishUp] )
261 - (void) _exited: (NSNotification*)n
263 _resultCode = _task.terminationStatus;
264 LogTo(Task,@"Exited with result=%i",_resultCode);
266 if( [self _shouldFinishUp] )
269 [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
275 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
278 LogTo(Task,@"Finished!");
280 if( _resultCode != 0 ) {
282 NSString *errStr = nil;
283 if( _errorData.length > 0 )
284 errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
285 Log(@" *** task returned %i: %@",_resultCode,errStr);
286 if( errStr.length == 0 )
287 errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
288 NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
289 // For some reason the body text in the alert shown by -presentError: is taken from the
290 // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
291 NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
292 desc, NSLocalizedDescriptionKey,
293 errStr, NSLocalizedRecoverySuggestionErrorKey,
294 [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
295 self, MYTaskObjectKey,
297 self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain
309 // This is a hook that subclasses can override to do post-processing.
313 - (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
315 // wait for task to exit:
316 while( _task.isRunning || self.isRunning )
317 [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
318 beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
319 return (_resultCode==0);
322 - (BOOL) waitTillFinished
324 return [self _waitTillFinishedInMode: _modes.lastObject];
330 [_modes addObject: MYTaskSynchronousRunLoopMode];
331 return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
336 - (BOOL) run: (NSError**)outError
338 BOOL result = [self run];
339 if( outError ) *outError = self.error;
344 @synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
349 if( ! _output && _outputData ) {
350 _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
351 // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
353 _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
354 Log(@"Warning: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
360 - (NSString*) outputAndError
362 NSString *result = self.output ?: @"";
363 NSString *errorStr = nil;
365 errorStr = [NSString stringWithFormat: @"%@:\n%@",
366 _error.localizedDescription,_error.localizedRecoverySuggestion];
367 else if( _errorData.length > 0 )
368 errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
370 result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
374 + (NSArray*) keyPathsForValuesAffectingOutputAndError
376 return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];