Added NSData categories for GZip and Mnemonicode.
Minor tweaks elsewhere.
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
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 - (NSString*) commandLine {
111 NSMutableString *desc = [NSMutableString stringWithString: _command];
112 for (NSString *arg in _arguments) {
113 [desc appendString: @" "];
114 if ([arg rangeOfString: @" "].length > 0)
115 arg = [NSString stringWithFormat: @"'%@'", arg];
116 [desc appendString: arg];
122 - (void) ignoreOutput
128 - (BOOL) makeError: (NSString*)fmt, ...
133 NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
134 LogTo(MYTask, @"Error: %@",message);
135 NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
136 forKey: NSLocalizedDescriptionKey];
137 _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
144 - (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
146 NSPipe *pipe = [NSPipe pipe];
147 *handle = [pipe fileHandleForReading];
148 [[NSNotificationCenter defaultCenter] addObserver: self selector: selector
149 name: NSFileHandleReadCompletionNotification
151 [*handle readInBackgroundAndNotifyForModes: _modes];
158 // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
161 [[NSNotificationCenter defaultCenter] removeObserver: self
162 name: NSFileHandleReadCompletionNotification
167 /** Subclasses can override this. */
168 - (NSTask*) createTask
170 Assert(!_task,@"createTask called twice");
171 NSTask *task = [[NSTask alloc] init];
172 task.launchPath = _command;
173 task.arguments = _arguments;
174 if( _currentDirectoryPath )
175 task.currentDirectoryPath = _currentDirectoryPath;
182 Assert(!_task, @"Task has already been run");
186 _task = [self createTask];
187 Assert(_task,@"createTask returned nil");
189 LogTo(MYTask,@"$ %@", self.commandLine);
191 _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
192 _outputData = [[NSMutableData alloc] init];
193 _task.standardError = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
194 _errorData = [[NSMutableData alloc] init];
196 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
197 name: NSTaskDidTerminateNotification
203 Warn(@"Task failed to launch: %@",x);
206 return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
209 self.isRunning = YES;
224 - (BOOL) _shouldFinishUp
226 return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
230 - (void) _gotOutput: (NSNotification*)n
232 NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
233 if( n.object == _outHandle ) {
234 if( data.length > 0 ) {
235 [_outHandle readInBackgroundAndNotifyForModes: _modes];
236 LogTo(HgTaskVerbose, @"Got %u bytes of output",data.length);
238 [self willChangeValueForKey: @"output"];
239 [self willChangeValueForKey: @"outputData"];
240 [_outputData appendData: data];
242 [self didChangeValueForKey: @"outputData"];
243 [self didChangeValueForKey: @"output"];
246 LogTo(HgTaskVerbose, @"Closed output");
248 if( [self _shouldFinishUp] )
254 - (void) _gotStderr: (NSNotification*)n
256 if( n.object == _errHandle ) {
257 NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
258 if( data.length > 0 ) {
259 [_errHandle readInBackgroundAndNotifyForModes: _modes];
260 LogTo(HgTaskVerbose, @"Got %u bytes of stderr",data.length);
261 [self willChangeValueForKey: @"errorData"];
262 [_errorData appendData: data];
263 [self didChangeValueForKey: @"errorData"];
265 LogTo(HgTaskVerbose, @"Closed stderr");
267 if( [self _shouldFinishUp] )
273 - (void) _exited: (NSNotification*)n
275 _resultCode = _task.terminationStatus;
276 LogTo(HgTaskVerbose, @"Exited with result=%i",_resultCode);
278 if( [self _shouldFinishUp] )
281 [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
287 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
290 LogTo(HgTaskVerbose, @"Finished!");
292 if( _resultCode != 0 ) {
294 NSString *errStr = nil;
295 if( _errorData.length > 0 )
296 errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
297 LogTo(MYTask, @" *** task returned %i: %@",_resultCode,errStr);
298 if( errStr.length == 0 )
299 errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
300 NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
301 // For some reason the body text in the alert shown by -presentError: is taken from the
302 // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
303 NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
304 desc, NSLocalizedDescriptionKey,
305 errStr, NSLocalizedRecoverySuggestionErrorKey,
306 [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
307 self, MYTaskObjectKey,
309 self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain
321 // This is a hook that subclasses can override to do post-processing.
325 - (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
327 // wait for task to exit:
328 while( _task.isRunning || self.isRunning )
329 [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
330 beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
331 return (_resultCode==0);
334 - (BOOL) waitTillFinished
336 return [self _waitTillFinishedInMode: _modes.lastObject];
342 [_modes addObject: MYTaskSynchronousRunLoopMode];
343 return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
348 - (BOOL) run: (NSError**)outError
350 BOOL result = [self run];
351 if( outError ) *outError = self.error;
356 @synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
361 if( ! _output && _outputData ) {
362 _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
363 // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
365 _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
366 Warn(@"MYTask: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
372 - (NSString*) outputAndError
374 NSString *result = self.output ?: @"";
375 NSString *errorStr = nil;
377 errorStr = [NSString stringWithFormat: @"%@:\n%@",
378 _error.localizedDescription,_error.localizedRecoverySuggestion];
379 else if( _errorData.length > 0 )
380 errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
382 result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
386 + (NSArray*) keyPathsForValuesAffectingOutputAndError
388 return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];