//// 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*constMYTaskErrorDomain=@"MYTaskError";NSString*constMYTaskExitCodeKey=@"MYTaskExitCode";NSString*constMYTaskObjectKey=@"MYTask";#define MYTaskSynchronousRunLoopMode @"MYTask"@interfaceMYTask()@property(readwrite,nonatomic)BOOLisRunning;@property(readwrite,retain,nonatomic)NSError*error;-(void)_finishUp;@end@implementationMYTask-(id)initWithCommand:(NSString*)commandarguments:(NSArray*)arguments{Assert(command);self=[superinit];if(self!=nil){_command=command;_arguments=arguments?[argumentsmutableCopy]:[NSMutableArrayarray];_modes=[NSMutableArrayarrayWithObjects:NSDefaultRunLoopMode,NSModalPanelRunLoopMode,nil];}returnself;}-(id)initWithCommand:(NSString*)command,...{NSMutableArray*arguments=[NSMutableArrayarray];va_listargs;va_start(args,command);idarg;while(nil!=(arg=va_arg(args,id)))[argumentsaddObject:[argdescription]];va_end(args);return[selfinitWithCommand:commandarguments:arguments];}-(id)initWithError:(NSError*)error{self=[superinit];if(self){_error=error;}returnself;}-(NSString*)description{return[NSStringstringWithFormat:@"%@ %@",_command,[_argumentscomponentsJoinedByString:@" "]];}-(void)addArgument:(id)argument{[_argumentsaddObject:[argumentdescription]];}-(void)addArgumentsFromArray:(NSArray*)arguments{for(idarginarguments)[_argumentsaddObject:[argdescription]];}-(void)addArguments:(id)arg,...{va_listargs;va_start(args,arg);while(arg){[_argumentsaddObject:[argdescription]];arg=va_arg(args,id);}va_end(args);}-(void)prependArguments:(id)arg,...{va_listargs;va_start(args,arg);inti=0;while(arg){[_argumentsinsertObject:[argdescription]atIndex:i++];arg=va_arg(args,id);}va_end(args);}-(NSString*)commandLine{NSMutableString*desc=[NSMutableStringstringWithString:_command];for(NSString*argin_arguments){[descappendString:@" "];if([argrangeOfString:@" "].length>0)arg=[NSStringstringWithFormat:@"'%@'",arg];[descappendString:arg];}returndesc;}-(void)ignoreOutput{_ignoreOutput=YES;}-(BOOL)makeError:(NSString*)fmt,...{va_listargs;va_start(args,fmt);NSString*message=[[NSStringalloc]initWithFormat:fmtarguments:args];LogTo(MYTask,@"Error: %@",message);NSMutableDictionary*info=[NSMutableDictionarydictionaryWithObject:messageforKey:NSLocalizedDescriptionKey];_error=[NSErrorerrorWithDomain:MYTaskErrorDomaincode:kMYTaskErroruserInfo:info];va_end(args);returnNO;}-(NSPipe*)_openPipeAndHandle:(NSFileHandle**)handlenotifying:(SEL)selector{NSPipe*pipe=[NSPipepipe];*handle=[pipefileHandleForReading];[[NSNotificationCenterdefaultCenter]addObserver:selfselector:selectorname:NSFileHandleReadCompletionNotificationobject:*handle];[*handlereadInBackgroundAndNotifyForModes:_modes];returnpipe;}-(void)_close{// No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)_outHandle=nil;_errHandle=nil;[[NSNotificationCenterdefaultCenter]removeObserver:selfname:NSFileHandleReadCompletionNotificationobject:nil];}/** Subclasses can override this. */-(NSTask*)createTask{Assert(!_task,@"createTask called twice");NSTask*task=[[NSTaskalloc]init];task.launchPath=_command;task.arguments=_arguments;if(_currentDirectoryPath)task.currentDirectoryPath=_currentDirectoryPath;returntask;}-(BOOL)start{Assert(!_task,@"Task has already been run");if(_error)returnNO;_task=[selfcreateTask];Assert(_task,@"createTask returned nil");LogTo(MYTask,@"$ %@",self.commandLine);_task.standardOutput=[self_openPipeAndHandle:&_outHandlenotifying:@selector(_gotOutput:)];_outputData=[[NSMutableDataalloc]init];_task.standardError=[self_openPipeAndHandle:&_errHandlenotifying:@selector(_gotStderr:)];_errorData=[[NSMutableDataalloc]init];[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(_exited:)name:NSTaskDidTerminateNotificationobject:_task];@try{[_tasklaunch];}@catch(idx){Warn(@"Task failed to launch: %@",x);_resultCode=666;[self_close];return[selfmakeError:@"Exception launching %@: %@",_task.launchPath,x];}LogTo(MYTaskVerbose,@"Launched task, modes %@",_modes);Assert(_task.isRunning);_taskRunning=YES;self.isRunning=YES;returnYES;}-(void)stop{LogTo(MYTaskVerbose,@"Stopping task");[_taskinterrupt];[self_close];_taskRunning=NO;self.isRunning=NO;}-(BOOL)_shouldFinishUp{return!_task.isRunning&&(_ignoreOutput||(!_outHandle&&!_errHandle));}-(void)_gotOutput:(NSNotification*)n{NSData*data=[n.userInfoobjectForKey:NSFileHandleNotificationDataItem];if(n.object==_outHandle){if(data.length>0){[_outHandlereadInBackgroundAndNotifyForModes:_modes];LogTo(MYTaskVerbose,@"Got %u bytes of output",data.length);if(_outputData){[selfwillChangeValueForKey:@"output"];[selfwillChangeValueForKey:@"outputData"];[_outputDataappendData:data];_output=nil;[selfdidChangeValueForKey:@"outputData"];[selfdidChangeValueForKey:@"output"];}}else{LogTo(MYTaskVerbose,@"Closed output");_outHandle=nil;if([self_shouldFinishUp])[self_finishUp];}}}-(void)_gotStderr:(NSNotification*)n{if(n.object==_errHandle){NSData*data=[n.userInfoobjectForKey:NSFileHandleNotificationDataItem];if(data.length>0){[_errHandlereadInBackgroundAndNotifyForModes:_modes];LogTo(MYTaskVerbose,@"Got %u bytes of stderr",data.length);[selfwillChangeValueForKey:@"errorData"];[_errorDataappendData:data];[selfdidChangeValueForKey:@"errorData"];}else{LogTo(MYTaskVerbose,@"Closed stderr");_errHandle=nil;if([self_shouldFinishUp])[self_finishUp];}}}-(void)_exited:(NSNotification*)n{_resultCode=_task.terminationStatus;LogTo(MYTaskVerbose,@"Exited with result=%i",_resultCode);_taskRunning=NO;if([self_shouldFinishUp])[self_finishUp];else[selfperformSelector:@selector(_finishUp)withObject:nilafterDelay:1.0];}-(void)_finishUp{[NSObjectcancelPreviousPerformRequestsWithTarget:selfselector:@selector(_finishUp)object:nil];[self_close];LogTo(MYTaskVerbose,@"Finished!");if(_resultCode!=0){// Handle errors:NSString*errStr=nil;if(_errorData.length>0)errStr=[[NSStringalloc]initWithData:_errorDataencoding:NSUTF8StringEncoding];LogTo(MYTask,@" *** task returned %i: %@",_resultCode,errStr);if(errStr.length==0)errStr=[NSStringstringWithFormat:@"Command returned status %i",_resultCode];NSString*desc=[NSStringstringWithFormat:@"%@ 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=[NSMutableDictionarydictionaryWithObjectsAndKeys:desc,NSLocalizedDescriptionKey,errStr,NSLocalizedRecoverySuggestionErrorKey,[NSNumbernumberWithInt:_resultCode],MYTaskExitCodeKey,self,MYTaskObjectKey,nil];self.error=[[NSErroralloc]initWithDomain:MYTaskErrorDomaincode:kMYTaskErroruserInfo:info];}[selffinished];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)if(![[NSRunLoopcurrentRunLoop]runMode:runLoopModebeforeDate:[NSDatedateWithTimeIntervalSinceNow:1.0]]){// This happens if both stderr and stdout are closed (leaving no NSFileHandles running// in this runloop mode) but the task hasn't yet notified me that it exited.// For some reason, in 10.6 the notification sometimes just doesn't appear, so poll// for it:if(_task.isRunning){Warn(@"MYTask _waitTillFinishedInMode: no runloop sources left for %@ mode; waiting...",runLoopMode);sleep(1);}else{Warn(@"MYTask _waitTillFinishedInMode: Task exited without notifying!");[self_exited:nil];}}elseLogTo(MYTaskVerbose,@"..._waitTillFinishedInMode still waiting...");return(_resultCode==0)&&!_error;}-(BOOL)waitTillFinished{return[self_waitTillFinishedInMode:_modes.lastObject];}-(BOOL)run{[_modesaddObject:MYTaskSynchronousRunLoopMode];return[selfstart]&&[self_waitTillFinishedInMode:MYTaskSynchronousRunLoopMode];}-(BOOL)run:(NSError**)outError{BOOLresult=[selfrun];if(outError)*outError=self.error;returnresult;}@synthesizecurrentDirectoryPath=_currentDirectoryPath,outputData=_outputData,error=_error,isRunning=_isRunning;-(NSString*)output{if(!_output&&_outputData){_output=[[NSStringalloc]initWithData:_outputDataencoding:NSUTF8StringEncoding];// If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.if(!_output){_output=[[NSStringalloc]initWithData:_outputDataencoding:NSWindowsCP1252StringEncoding];Warn(@"MYTask: 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=[NSStringstringWithFormat:@"%@:\n%@",_error.localizedDescription,_error.localizedRecoverySuggestion];elseif(_errorData.length>0)errorStr=[[NSStringalloc]initWithData:_errorDataencoding:NSUTF8StringEncoding];if(errorStr)result=[NSStringstringWithFormat:@"%@\n\n%@",errorStr,result];returnresult;}+(NSArray*)keyPathsForValuesAffectingOutputAndError{return[NSArrayarrayWithObjects:@"output",@"error",@"errorData",nil];}@end