jens@20
|
1 |
//
|
jens@20
|
2 |
// MYTask.m
|
jens@20
|
3 |
// Murky
|
jens@20
|
4 |
//
|
jens@20
|
5 |
// Copyright 2008 Jens Alfke. All rights reserved.
|
jens@20
|
6 |
//
|
jens@20
|
7 |
|
jens@20
|
8 |
#import "MYTask.h"
|
jens@20
|
9 |
|
jens@20
|
10 |
//FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it.
|
jens@20
|
11 |
|
jens@20
|
12 |
|
jens@20
|
13 |
NSString* const MYTaskErrorDomain = @"MYTaskError";
|
jens@20
|
14 |
NSString* const MYTaskExitCodeKey = @"MYTaskExitCode";
|
jens@20
|
15 |
NSString* const MYTaskObjectKey = @"MYTask";
|
jens@20
|
16 |
|
jens@20
|
17 |
#define MYTaskSynchronousRunLoopMode @"MYTask"
|
jens@20
|
18 |
|
jens@20
|
19 |
|
jens@20
|
20 |
@interface MYTask ()
|
jens@20
|
21 |
@property (readwrite,nonatomic) BOOL isRunning;
|
jens@20
|
22 |
@property (readwrite,retain,nonatomic) NSError *error;
|
jens@20
|
23 |
- (void) _finishUp;
|
jens@20
|
24 |
@end
|
jens@20
|
25 |
|
jens@20
|
26 |
|
jens@20
|
27 |
@implementation MYTask
|
jens@20
|
28 |
|
jens@20
|
29 |
|
jens@20
|
30 |
- (id) initWithCommand: (NSString*)command
|
jens@20
|
31 |
arguments: (NSArray*)arguments
|
jens@20
|
32 |
{
|
jens@27
|
33 |
Assert(command);
|
jens@20
|
34 |
self = [super init];
|
jens@20
|
35 |
if (self != nil) {
|
jens@20
|
36 |
_command = command;
|
jens@20
|
37 |
_arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array];
|
jens@20
|
38 |
_modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil];
|
jens@20
|
39 |
}
|
jens@20
|
40 |
return self;
|
jens@20
|
41 |
}
|
jens@20
|
42 |
|
jens@20
|
43 |
|
jens@20
|
44 |
- (id) initWithCommand: (NSString*)command, ...
|
jens@20
|
45 |
{
|
jens@20
|
46 |
NSMutableArray *arguments = [NSMutableArray array];
|
jens@20
|
47 |
va_list args;
|
jens@20
|
48 |
va_start(args,command);
|
jens@20
|
49 |
id arg;
|
jens@20
|
50 |
while( nil != (arg=va_arg(args,id)) )
|
jens@20
|
51 |
[arguments addObject: [arg description]];
|
jens@20
|
52 |
va_end(args);
|
jens@20
|
53 |
|
jens@20
|
54 |
return [self initWithCommand: command arguments: arguments];
|
jens@20
|
55 |
}
|
jens@20
|
56 |
|
jens@20
|
57 |
|
jens@20
|
58 |
- (id) initWithError: (NSError*)error
|
jens@20
|
59 |
{
|
jens@20
|
60 |
self = [super init];
|
jens@20
|
61 |
if( self ) {
|
jens@20
|
62 |
_error = error;
|
jens@20
|
63 |
}
|
jens@20
|
64 |
return self;
|
jens@20
|
65 |
}
|
jens@20
|
66 |
|
jens@20
|
67 |
|
jens@20
|
68 |
- (NSString*) description
|
jens@20
|
69 |
{
|
jens@20
|
70 |
return [NSString stringWithFormat: @"%@ %@",
|
jens@20
|
71 |
_command, [_arguments componentsJoinedByString: @" "]];
|
jens@20
|
72 |
}
|
jens@20
|
73 |
|
jens@20
|
74 |
|
jens@20
|
75 |
- (void) addArgument: (id)argument
|
jens@20
|
76 |
{
|
jens@20
|
77 |
[_arguments addObject: [argument description]];
|
jens@20
|
78 |
}
|
jens@20
|
79 |
|
jens@20
|
80 |
- (void) addArgumentsFromArray: (NSArray*)arguments
|
jens@20
|
81 |
{
|
jens@20
|
82 |
for( id arg in arguments )
|
jens@20
|
83 |
[_arguments addObject: [arg description]];
|
jens@20
|
84 |
}
|
jens@20
|
85 |
|
jens@20
|
86 |
- (void) addArguments: (id)arg, ...
|
jens@20
|
87 |
{
|
jens@20
|
88 |
va_list args;
|
jens@20
|
89 |
va_start(args,arg);
|
jens@20
|
90 |
while( arg ) {
|
jens@20
|
91 |
[_arguments addObject: [arg description]];
|
jens@20
|
92 |
arg = va_arg(args,id);
|
jens@20
|
93 |
}
|
jens@20
|
94 |
va_end(args);
|
jens@20
|
95 |
}
|
jens@20
|
96 |
|
jens@20
|
97 |
- (void) prependArguments: (id)arg, ...
|
jens@20
|
98 |
{
|
jens@20
|
99 |
va_list args;
|
jens@20
|
100 |
va_start(args,arg);
|
jens@20
|
101 |
int i=0;
|
jens@20
|
102 |
while( arg ) {
|
jens@20
|
103 |
[_arguments insertObject: [arg description] atIndex: i++];
|
jens@20
|
104 |
arg = va_arg(args,id);
|
jens@20
|
105 |
}
|
jens@20
|
106 |
va_end(args);
|
jens@20
|
107 |
}
|
jens@20
|
108 |
|
jens@20
|
109 |
|
jens@27
|
110 |
- (NSString*) commandLine {
|
jens@27
|
111 |
NSMutableString *desc = [NSMutableString stringWithString: _command];
|
jens@27
|
112 |
for (NSString *arg in _arguments) {
|
jens@27
|
113 |
[desc appendString: @" "];
|
jens@27
|
114 |
if ([arg rangeOfString: @" "].length > 0)
|
jens@27
|
115 |
arg = [NSString stringWithFormat: @"'%@'", arg];
|
jens@27
|
116 |
[desc appendString: arg];
|
jens@27
|
117 |
}
|
jens@27
|
118 |
return desc;
|
jens@27
|
119 |
}
|
jens@27
|
120 |
|
jens@27
|
121 |
|
jens@20
|
122 |
- (void) ignoreOutput
|
jens@20
|
123 |
{
|
jens@20
|
124 |
_ignoreOutput = YES;
|
jens@20
|
125 |
}
|
jens@20
|
126 |
|
jens@20
|
127 |
|
jens@20
|
128 |
- (BOOL) makeError: (NSString*)fmt, ...
|
jens@20
|
129 |
{
|
jens@20
|
130 |
va_list args;
|
jens@20
|
131 |
va_start(args,fmt);
|
jens@20
|
132 |
|
jens@20
|
133 |
NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
|
jens@27
|
134 |
LogTo(MYTask, @"Error: %@",message);
|
jens@20
|
135 |
NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
|
jens@20
|
136 |
forKey: NSLocalizedDescriptionKey];
|
jens@20
|
137 |
_error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
|
jens@20
|
138 |
|
jens@20
|
139 |
va_end(args);
|
jens@20
|
140 |
return NO;
|
jens@20
|
141 |
}
|
jens@20
|
142 |
|
jens@20
|
143 |
|
jens@20
|
144 |
- (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
|
jens@20
|
145 |
{
|
jens@20
|
146 |
NSPipe *pipe = [NSPipe pipe];
|
jens@20
|
147 |
*handle = [pipe fileHandleForReading];
|
jens@20
|
148 |
[[NSNotificationCenter defaultCenter] addObserver: self selector: selector
|
jens@20
|
149 |
name: NSFileHandleReadCompletionNotification
|
jens@20
|
150 |
object: *handle];
|
jens@20
|
151 |
[*handle readInBackgroundAndNotifyForModes: _modes];
|
jens@20
|
152 |
return pipe;
|
jens@20
|
153 |
}
|
jens@20
|
154 |
|
jens@20
|
155 |
|
jens@20
|
156 |
- (void) _close
|
jens@20
|
157 |
{
|
jens@20
|
158 |
// No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
|
jens@20
|
159 |
_outHandle = nil;
|
jens@20
|
160 |
_errHandle = nil;
|
jens@20
|
161 |
[[NSNotificationCenter defaultCenter] removeObserver: self
|
jens@20
|
162 |
name: NSFileHandleReadCompletionNotification
|
jens@20
|
163 |
object: nil];
|
jens@20
|
164 |
}
|
jens@20
|
165 |
|
jens@20
|
166 |
|
jens@20
|
167 |
/** Subclasses can override this. */
|
jens@20
|
168 |
- (NSTask*) createTask
|
jens@20
|
169 |
{
|
jens@27
|
170 |
Assert(!_task,@"createTask called twice");
|
jens@20
|
171 |
NSTask *task = [[NSTask alloc] init];
|
jens@20
|
172 |
task.launchPath = _command;
|
jens@20
|
173 |
task.arguments = _arguments;
|
jens@20
|
174 |
if( _currentDirectoryPath )
|
jens@20
|
175 |
task.currentDirectoryPath = _currentDirectoryPath;
|
jens@20
|
176 |
return task;
|
jens@20
|
177 |
}
|
jens@20
|
178 |
|
jens@20
|
179 |
|
jens@20
|
180 |
- (BOOL) start
|
jens@20
|
181 |
{
|
jens@27
|
182 |
Assert(!_task, @"Task has already been run");
|
jens@20
|
183 |
if( _error )
|
jens@20
|
184 |
return NO;
|
jens@20
|
185 |
|
jens@20
|
186 |
_task = [self createTask];
|
jens@27
|
187 |
Assert(_task,@"createTask returned nil");
|
jens@20
|
188 |
|
jens@27
|
189 |
LogTo(MYTask,@"$ %@", self.commandLine);
|
jens@20
|
190 |
|
jens@20
|
191 |
_task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
|
jens@20
|
192 |
_outputData = [[NSMutableData alloc] init];
|
jens@20
|
193 |
_task.standardError = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
|
jens@20
|
194 |
_errorData = [[NSMutableData alloc] init];
|
jens@20
|
195 |
|
jens@20
|
196 |
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
|
jens@20
|
197 |
name: NSTaskDidTerminateNotification
|
jens@20
|
198 |
object: _task];
|
jens@20
|
199 |
|
jens@20
|
200 |
@try{
|
jens@20
|
201 |
[_task launch];
|
jens@20
|
202 |
}@catch( id x ) {
|
jens@27
|
203 |
Warn(@"Task failed to launch: %@",x);
|
jens@20
|
204 |
_resultCode = 666;
|
jens@20
|
205 |
[self _close];
|
jens@20
|
206 |
return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
|
jens@20
|
207 |
}
|
jens@20
|
208 |
_taskRunning = YES;
|
jens@20
|
209 |
self.isRunning = YES;
|
jens@20
|
210 |
|
jens@20
|
211 |
return YES;
|
jens@20
|
212 |
}
|
jens@20
|
213 |
|
jens@20
|
214 |
|
jens@20
|
215 |
- (void) stop
|
jens@20
|
216 |
{
|
jens@20
|
217 |
[_task interrupt];
|
jens@20
|
218 |
[self _close];
|
jens@20
|
219 |
_taskRunning = NO;
|
jens@20
|
220 |
self.isRunning = NO;
|
jens@20
|
221 |
}
|
jens@20
|
222 |
|
jens@20
|
223 |
|
jens@20
|
224 |
- (BOOL) _shouldFinishUp
|
jens@20
|
225 |
{
|
jens@20
|
226 |
return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
|
jens@20
|
227 |
}
|
jens@20
|
228 |
|
jens@20
|
229 |
|
jens@20
|
230 |
- (void) _gotOutput: (NSNotification*)n
|
jens@20
|
231 |
{
|
jens@20
|
232 |
NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
|
jens@20
|
233 |
if( n.object == _outHandle ) {
|
jens@20
|
234 |
if( data.length > 0 ) {
|
jens@20
|
235 |
[_outHandle readInBackgroundAndNotifyForModes: _modes];
|
jens@27
|
236 |
LogTo(HgTaskVerbose, @"Got %u bytes of output",data.length);
|
jens@20
|
237 |
if( _outputData ) {
|
jens@20
|
238 |
[self willChangeValueForKey: @"output"];
|
jens@20
|
239 |
[self willChangeValueForKey: @"outputData"];
|
jens@20
|
240 |
[_outputData appendData: data];
|
jens@20
|
241 |
_output = nil;
|
jens@20
|
242 |
[self didChangeValueForKey: @"outputData"];
|
jens@20
|
243 |
[self didChangeValueForKey: @"output"];
|
jens@20
|
244 |
}
|
jens@20
|
245 |
} else {
|
jens@27
|
246 |
LogTo(HgTaskVerbose, @"Closed output");
|
jens@20
|
247 |
_outHandle = nil;
|
jens@20
|
248 |
if( [self _shouldFinishUp] )
|
jens@20
|
249 |
[self _finishUp];
|
jens@20
|
250 |
}
|
jens@20
|
251 |
}
|
jens@20
|
252 |
}
|
jens@20
|
253 |
|
jens@20
|
254 |
- (void) _gotStderr: (NSNotification*)n
|
jens@20
|
255 |
{
|
jens@20
|
256 |
if( n.object == _errHandle ) {
|
jens@20
|
257 |
NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
|
jens@20
|
258 |
if( data.length > 0 ) {
|
jens@20
|
259 |
[_errHandle readInBackgroundAndNotifyForModes: _modes];
|
jens@27
|
260 |
LogTo(HgTaskVerbose, @"Got %u bytes of stderr",data.length);
|
jens@20
|
261 |
[self willChangeValueForKey: @"errorData"];
|
jens@20
|
262 |
[_errorData appendData: data];
|
jens@20
|
263 |
[self didChangeValueForKey: @"errorData"];
|
jens@20
|
264 |
} else {
|
jens@27
|
265 |
LogTo(HgTaskVerbose, @"Closed stderr");
|
jens@20
|
266 |
_errHandle = nil;
|
jens@20
|
267 |
if( [self _shouldFinishUp] )
|
jens@20
|
268 |
[self _finishUp];
|
jens@20
|
269 |
}
|
jens@20
|
270 |
}
|
jens@20
|
271 |
}
|
jens@20
|
272 |
|
jens@20
|
273 |
- (void) _exited: (NSNotification*)n
|
jens@20
|
274 |
{
|
jens@20
|
275 |
_resultCode = _task.terminationStatus;
|
jens@27
|
276 |
LogTo(HgTaskVerbose, @"Exited with result=%i",_resultCode);
|
jens@20
|
277 |
_taskRunning = NO;
|
jens@20
|
278 |
if( [self _shouldFinishUp] )
|
jens@20
|
279 |
[self _finishUp];
|
jens@20
|
280 |
else
|
jens@20
|
281 |
[self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
|
jens@20
|
282 |
}
|
jens@20
|
283 |
|
jens@20
|
284 |
|
jens@20
|
285 |
- (void) _finishUp
|
jens@20
|
286 |
{
|
jens@20
|
287 |
[NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
|
jens@20
|
288 |
[self _close];
|
jens@20
|
289 |
|
jens@27
|
290 |
LogTo(HgTaskVerbose, @"Finished!");
|
jens@20
|
291 |
|
jens@20
|
292 |
if( _resultCode != 0 ) {
|
jens@20
|
293 |
// Handle errors:
|
jens@20
|
294 |
NSString *errStr = nil;
|
jens@20
|
295 |
if( _errorData.length > 0 )
|
jens@20
|
296 |
errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
|
jens@27
|
297 |
LogTo(MYTask, @" *** task returned %i: %@",_resultCode,errStr);
|
jens@20
|
298 |
if( errStr.length == 0 )
|
jens@20
|
299 |
errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
|
jens@20
|
300 |
NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
|
jens@20
|
301 |
// For some reason the body text in the alert shown by -presentError: is taken from the
|
jens@20
|
302 |
// NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
|
jens@20
|
303 |
NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
|
jens@20
|
304 |
desc, NSLocalizedDescriptionKey,
|
jens@20
|
305 |
errStr, NSLocalizedRecoverySuggestionErrorKey,
|
jens@20
|
306 |
[NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
|
jens@20
|
307 |
self, MYTaskObjectKey,
|
jens@20
|
308 |
nil];
|
jens@20
|
309 |
self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain
|
jens@20
|
310 |
code: kMYTaskError
|
jens@20
|
311 |
userInfo: info];
|
jens@20
|
312 |
}
|
jens@20
|
313 |
|
jens@20
|
314 |
[self finished];
|
jens@20
|
315 |
|
jens@20
|
316 |
self.isRunning = NO;
|
jens@20
|
317 |
}
|
jens@20
|
318 |
|
jens@20
|
319 |
- (void) finished
|
jens@20
|
320 |
{
|
jens@20
|
321 |
// This is a hook that subclasses can override to do post-processing.
|
jens@20
|
322 |
}
|
jens@20
|
323 |
|
jens@20
|
324 |
|
jens@20
|
325 |
- (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
|
jens@20
|
326 |
{
|
jens@20
|
327 |
// wait for task to exit:
|
jens@20
|
328 |
while( _task.isRunning || self.isRunning )
|
jens@20
|
329 |
[[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
|
jens@20
|
330 |
beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
|
jens@20
|
331 |
return (_resultCode==0);
|
jens@20
|
332 |
}
|
jens@20
|
333 |
|
jens@20
|
334 |
- (BOOL) waitTillFinished
|
jens@20
|
335 |
{
|
jens@20
|
336 |
return [self _waitTillFinishedInMode: _modes.lastObject];
|
jens@20
|
337 |
}
|
jens@20
|
338 |
|
jens@20
|
339 |
|
jens@20
|
340 |
- (BOOL) run
|
jens@20
|
341 |
{
|
jens@20
|
342 |
[_modes addObject: MYTaskSynchronousRunLoopMode];
|
jens@20
|
343 |
return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
|
jens@20
|
344 |
|
jens@20
|
345 |
}
|
jens@20
|
346 |
|
jens@20
|
347 |
|
jens@20
|
348 |
- (BOOL) run: (NSError**)outError
|
jens@20
|
349 |
{
|
jens@20
|
350 |
BOOL result = [self run];
|
jens@20
|
351 |
if( outError ) *outError = self.error;
|
jens@20
|
352 |
return result;
|
jens@20
|
353 |
}
|
jens@20
|
354 |
|
jens@20
|
355 |
|
jens@20
|
356 |
@synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
|
jens@20
|
357 |
|
jens@20
|
358 |
|
jens@20
|
359 |
- (NSString*) output
|
jens@20
|
360 |
{
|
jens@20
|
361 |
if( ! _output && _outputData ) {
|
jens@20
|
362 |
_output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
|
jens@20
|
363 |
// If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
|
jens@20
|
364 |
if( ! _output ) {
|
jens@20
|
365 |
_output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
|
jens@27
|
366 |
Warn(@"MYTask: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
|
jens@20
|
367 |
}
|
jens@20
|
368 |
}
|
jens@20
|
369 |
return _output;
|
jens@20
|
370 |
}
|
jens@20
|
371 |
|
jens@20
|
372 |
- (NSString*) outputAndError
|
jens@20
|
373 |
{
|
jens@20
|
374 |
NSString *result = self.output ?: @"";
|
jens@20
|
375 |
NSString *errorStr = nil;
|
jens@20
|
376 |
if( _error )
|
jens@20
|
377 |
errorStr = [NSString stringWithFormat: @"%@:\n%@",
|
jens@20
|
378 |
_error.localizedDescription,_error.localizedRecoverySuggestion];
|
jens@20
|
379 |
else if( _errorData.length > 0 )
|
jens@20
|
380 |
errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
|
jens@20
|
381 |
if( errorStr )
|
jens@20
|
382 |
result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
|
jens@20
|
383 |
return result;
|
jens@20
|
384 |
}
|
jens@20
|
385 |
|
jens@20
|
386 |
+ (NSArray*) keyPathsForValuesAffectingOutputAndError
|
jens@20
|
387 |
{
|
jens@20
|
388 |
return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];
|
jens@20
|
389 |
}
|
jens@20
|
390 |
|
jens@20
|
391 |
|
jens@20
|
392 |
@end
|