-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathSSYShellTasker.m
351 lines (296 loc) · 10.9 KB
/
SSYShellTasker.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#import "SSYShellTasker.h"
#import "NSError+InfoAccess.h"
#import "SSYThreadPauser.h"
#import "SSYRunLoopTickler.h"
NSInteger const SSYShellTaskerErrorFailedLaunch = 90551 ;
NSInteger const SSYShellTaskerErrorTimedOut = 90552 ;
NSString* const constKeySSYShellTaskerCommand = @"command" ;
NSString* const constKeySSYShellTaskerArguments = @"arguments" ;
NSString* const constKeySSYShellTaskerInDirectory = @"inDirectory" ;
NSString* const constKeySSYShellTaskerStdinData = @"stdinData" ;
NSString* const constKeySSYShellTaskerStdoutData = @"stdoutData" ;
NSString* const constKeySSYShellTaskerStderrData = @"stderrData" ;
NSString* const constKeySSYShellTaskerTimeout = @"timeout" ;
NSString* const constKeySSYShellTaskerResult = @"result" ;
NSString* const constKeySSYShellTaskerNSError = @"error" ;
NSString* const constKeySSYShellTaskerWants = @"wants" ;
// Since stdout and stderr might be huge, for efficiency, we
// only provide them if the invoker wants them.
#define SSYShellTaskerWantsStdout 0x1
#define SSYShellTaskerWantsStderr 0x2
// The task "result" are always provided, since they are small
@implementation SSYShellTasker
- (void)taskDone:(NSNotification*)note {
// This is definitely needed in Mac OS 10.6:
[SSYRunLoopTickler tickle] ;
}
- (void)doWithInfo:(NSMutableDictionary*)info {
// Each of the three NSFileHandles we are going to create requires creation of an NSPipe,
// which according to documentation -fileHandleForReading is released "automatically"
// when the NSPipe is released. Actually, I find that it is autoreleased when the
// current autorelease pool is released, which is a little different.
// To conserve system resources, therefore, we use a local pool here.
// For more info,
// http://www.cocoabuilder.com/archive/message/cocoa/2002/11/30/51122
#if !__has_feature(objc_arc)
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init] ;
#endif
NSString* command = [info objectForKey:constKeySSYShellTaskerCommand] ;
NSArray* arguments = [info objectForKey:constKeySSYShellTaskerArguments] ;
NSString* inDirectory = [info objectForKey:constKeySSYShellTaskerInDirectory] ;
NSData* stdinData = [info objectForKey:constKeySSYShellTaskerStdinData] ;
NSTimeInterval timeout = [[info objectForKey:constKeySSYShellTaskerTimeout] doubleValue] ;
NSInteger wants = [[info objectForKey:constKeySSYShellTaskerWants] integerValue] ;
NSError* error = nil ;
NSInteger taskResult = 0 ;
NSTask* task;
NSPipe* pipeStdin = nil ;
NSPipe* pipeStdout = nil ;
NSPipe* pipeStderr = nil ;
NSFileHandle* fileStdin = nil ;
NSFileHandle* fileStdout = nil ;
NSFileHandle* fileStderr = nil ;
task = [[NSTask alloc] init] ;
[task setLaunchPath:command] ;
// The following section was added in BookMacster 1.12 to stop annoying
// warnings in Xcode console when running in Xcode:
// dyld: DYLD_ environment variables being ignored because main executable (/bin/ps) is setuid or setgid
// This was suggested by Ken Thomases here…
// http://lists.apple.com/archives/xcode-users/2012/Sep/msg00022.html
if ([command hasSuffix:@"/ps"]) {
NSDictionary* environment = [[NSProcessInfo processInfo] environment] ;
NSMutableDictionary* taskEnvironment = [[NSMutableDictionary alloc] init] ;
for (NSString* key in environment) {
if (![key hasPrefix:@"DYLD_"]) {
[taskEnvironment setObject:[environment valueForKey:key]
forKey:key] ;
}
}
[task setEnvironment:taskEnvironment] ;
#if !__has_feature(objc_arc)
[taskEnvironment release] ;
#endif
}
if (inDirectory) {
[task setCurrentDirectoryPath:inDirectory] ;
}
if (arguments != nil) {
[task setArguments: arguments] ;
}
if (stdinData) {
pipeStdin = [[NSPipe alloc] init] ;
fileStdin = [pipeStdin fileHandleForWriting] ;
[task setStandardInput:pipeStdin] ;
}
if ((wants & SSYShellTaskerWantsStdout) > 0) {
pipeStdout = [[NSPipe alloc] init] ;
fileStdout = [pipeStdout fileHandleForReading] ;
[task setStandardOutput:pipeStdout ] ;
}
if ((wants & SSYShellTaskerWantsStderr) > 0) {
pipeStderr = [[NSPipe alloc] init] ;
fileStderr = [pipeStderr fileHandleForReading] ;
[task setStandardError:pipeStderr ] ;
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(taskDone:)
name:NSTaskDidTerminateNotification
object:task] ;
@try {
[task launch] ;
if ([task isRunning]) {
// Note: The following won't execute if no stdinData, since fileStdin will be nil
[fileStdin writeData:stdinData] ;
[fileStdin closeFile] ;
}
if (timeout > 0.0) {
NSDate* limitTime = [NSDate dateWithTimeIntervalSinceNow:timeout] ;
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:limitTime] ;
// The above will block and be run to here either due to
// the posting of an NSTaskDidTerminateNotification, or the
// passing of limitTime, whichever occurs first.
if (![task isRunning]) {
taskResult = [task terminationStatus] ;
NSData* data ;
if ((wants & SSYShellTaskerWantsStdout) > 0) {
data = [fileStdout readDataToEndOfFile] ;
if (data) {
[info setObject:data
forKey:constKeySSYShellTaskerStdoutData] ;
}
}
if ((wants & SSYShellTaskerWantsStderr) > 0) {
data = [fileStderr readDataToEndOfFile] ;
if (data) {
[info setObject:data
forKey:constKeySSYShellTaskerStderrData] ;
}
}
}
else {
taskResult = SSYShellTaskerErrorTimedOut ;
// Clean up
kill([task processIdentifier], SIGKILL) ;
error = [NSError errorWithDomain:@"SSYShellTasker"
code:SSYShellTaskerErrorTimedOut
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
@"SSYShellTasker: Task timed out (and was killed)", NSLocalizedDescriptionKey,
[info objectForKey:constKeySSYShellTaskerTimeout], constKeySSYShellTaskerTimeout,
command, @"command",
nil]] ;
if (arguments) {
error = [error errorByAddingUserInfoObject:arguments
forKey:@"arguments"] ;
}
if (stdinData) {
error = [error errorByAddingUserInfoObject:stdinData
forKey:@"stdin data"] ;
}
}
}
}
@catch (NSException* exception) {
error = [NSError errorWithDomain:@"SSYShellTasker"
code:SSYShellTaskerErrorFailedLaunch
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
@"SSYShellTasker: Task raised exception attempting to launch", NSLocalizedDescriptionKey,
nil]] ;
error = [error errorByAddingUnderlyingException:exception] ;
taskResult = SSYShellTaskerErrorFailedLaunch ;
}
@finally {
}
[info setObject:[NSNumber numberWithInteger:taskResult]
forKey:constKeySSYShellTaskerResult] ;
if (error) {
[info setObject:error
forKey:constKeySSYShellTaskerNSError] ;
}
#if !__has_feature(objc_arc)
[pipeStdin release] ;
[pipeStdout release] ;
[pipeStderr release] ;
#endif
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSTaskDidTerminateNotification
object:task] ;
#if !__has_feature(objc_arc)
[task release] ;
[pool release] ;
#endif
}
+ (NSInteger)doShellTaskCommand:(NSString*)command
arguments:(NSArray*)arguments
inDirectory:(NSString*)inDirectory
stdinData:(NSData*)stdinData
stdoutData_p:(NSData**)stdoutData_p
stderrData_p:(NSData**)stderrData_p
timeout:(NSTimeInterval)timeout
error_p:(NSError**)error_p {
NSInteger wants = 0 ;
if (stdoutData_p) {
wants += SSYShellTaskerWantsStdout ;
}
if (stderrData_p) {
wants += SSYShellTaskerWantsStderr ;
}
// Initialize dictionary with values that cannot be nil
NSMutableDictionary* info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
command, constKeySSYShellTaskerCommand,
[NSNumber numberWithInteger:wants], constKeySSYShellTaskerWants,
[NSNumber numberWithDouble:timeout], constKeySSYShellTaskerTimeout,
nil] ;
// Now set in values which can be nil
if (arguments) {
[info setObject:arguments
forKey:constKeySSYShellTaskerArguments] ;
}
if (inDirectory) {
[info setObject:inDirectory
forKey:constKeySSYShellTaskerInDirectory] ;
}
if (stdinData) {
[info setObject:stdinData
forKey:constKeySSYShellTaskerStdinData] ;
}
SSYShellTasker* tasker = [[SSYShellTasker alloc] init] ;
NSInteger result ;
if (timeout == 0.0) {
[tasker doWithInfo:info] ;
}
else {
[SSYThreadPauser blockUntilWorker:tasker
selector:@selector(doWithInfo:)
object:info
timeout:CGFLOAT_MAX] ;
// In the above, we set timeout:FLT_MAX because timeout is in info,
// and we'll get a more descriptive NSError if doWithInfo: times out
// than from SSYThreadPauser if we would let SSYThreadPauser time out.
// Also, doWithInfo: will clean up by killing the task process.
if (stdoutData_p) {
*stdoutData_p = [info objectForKey:constKeySSYShellTaskerStdoutData] ;
}
if (stderrData_p) {
*stderrData_p = [info objectForKey:constKeySSYShellTaskerStderrData] ;
}
if (error_p) {
*error_p = [info objectForKey:constKeySSYShellTaskerNSError] ;
}
}
result = [[info objectForKey:constKeySSYShellTaskerResult] integerValue] ;
#if !__has_feature(objc_arc)
[tasker release] ;
#endif
return result ;
}
@end
/* MORE TEST CODE for SSYShellTasker */
/*
#import "SSYShellTasker.h"
@interface Foo : NSObject {}
@end
@implementation Foo
+ (void)goShellStuff {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init] ;
NSError* error = nil ;
NSString* command ;
NSArray* arguments = nil ;
NSString* directory = nil ;
NSData* stdoutData = nil ;
command = @"/usr/bin/open" ;
arguments = [NSArray arrayWithObject:@"/Users/jk/Documents/Programming/Builds/Debug/BookMacster.app"] ;
command = @"/bin/ls" ;
arguments = nil ;
directory = @"/Users" ;
NSInteger result = [SSYShellTasker doShellTaskCommand:command
arguments:arguments
inDirectory:directory
stdinData:nil
stdoutData_p:&stdoutData
stderrData_p:NULL
timeout:5.0
error_p:&error] ;
NSLog(@"task result = %d", result) ;
NSString* stdoutString = [[NSString alloc] initWithData:stdoutData
encoding:NSUTF8StringEncoding] ;
NSLog(@"stdout:\n%@", stdoutString) ;
[stdoutString release] ;
NSLog(@"task error = %@", error) ;
[pool release] ;
}
@end
int main(int argc, const char *argv[]) {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init] ;
NSLog(@"----- Doing From Main Thread -----") ;
[Foo goShellStuff] ;
NSLog(@"----- Doing From Secondary Thread -----") ;
[NSThread detachNewThreadSelector:@selector(goShellStuff)
toTarget:[Foo class]
withObject:nil] ;
[[NSRunLoop currentRunLoop] run] ;
NSLog(@"This never executes.") ;
[pool release] ; // Needed to suppress compiler warning
return 0 ;
}
*/