-
Notifications
You must be signed in to change notification settings - Fork 1
/
SCTask.m
447 lines (366 loc) · 13.6 KB
/
SCTask.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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
/*
SCTask.m
NSTask subclass to encapsulate System specific extensions.
Copyright (C) 2006 Quentin Mathe
Author: Quentin Mathe <[email protected]>
Date: December 2006
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#import <EtoileFoundation/NSInvocation+Etoile.h>
#import <AppKit/AppKit.h> // For NSWorkspaceDidLaunchNotification
#import "SCTask.h"
#import "EtoileSystem.h"
#include <math.h>
@interface SCSystem (Private)
- (void) noteApplicationLaunched: (NSNotification *)notif;
@end
@interface SCTask (Private)
- (void) postTaskLaunched;
- (void) taskLaunched: (NSNotification *)notif;
- (void) taskTerminated: (NSNotification *)notif;
@end
@implementation SCTask
+ (NSString *) pathForName: (NSString *)name
{
NSMutableArray *searchPaths = [NSMutableArray array];
NSFileManager *fm = [NSFileManager defaultManager];
NSEnumerator *e;
NSString *path = nil;
BOOL isDir;
[searchPaths addObjectsFromArray:
NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
NSAllDomainsMask, YES)];
[searchPaths addObjectsFromArray:
NSSearchPathForDirectoriesInDomains(GSToolsDirectory,
NSAllDomainsMask, YES)];
NSLog(@"Searching for tool or application inside paths: %@", searchPaths);
e = [searchPaths objectEnumerator];
while ((path = [e nextObject]) != nil)
{
/* -stringByStandardizingPath removes double-slash, they occurs when
GNUSTEP_SYSTEM_ROOT is equal to '/' */
path = [[path stringByAppendingPathComponent: name] stringByStandardizingPath];
if ([fm fileExistsAtPath: path isDirectory: &isDir])
{
NSLog(@"Found tool or application at path: %@", path);
return path;
}
path = [path stringByAppendingPathExtension: @"app"];
if ([fm fileExistsAtPath: path isDirectory: &isDir])
{
NSLog(@"Found tool or application at path: %@", path);
return path;
}
}
return nil;
}
+ (SCTask *) taskWithLaunchPath: (NSString *)path
{
return [self taskWithLaunchPath: path priority: 0 onStart: YES onDemand: NO
withUserName: nil];
}
+ (SCTask *) taskWithLaunchPath: (NSString *)path priority: (int)level
onStart: (BOOL)now onDemand: (BOOL)lazily withUserName: (NSString *)identity
{
SCTask *newTask = [[SCTask alloc] init];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir;
/* If a name has been given as the launch path, we try to convert it to a
path. */
if ([[path pathComponents] count] == 1)
{
NSString *pathFound = [SCTask pathForName: path];
if (pathFound != nil)
{
ASSIGN(newTask->path, pathFound);
}
else
{
NSLog(@"WARNING: SCTask does not found a path for tool or "
@"application name: %@", path);
RELEASE(newTask);
return nil;
}
}
else
{
ASSIGN(newTask->path, path);
}
/* We check whether the launch path references an executable path or just
a directory which could be a potential application bundle. */
if ([fm fileExistsAtPath: [newTask path] isDirectory: &isDir] && isDir)
{
NSBundle *bundle = [NSBundle bundleWithPath: [newTask path]];
if (bundle != nil)
{
[newTask setLaunchPath: [bundle executablePath]];
}
else
{
NSLog(@"WARNING: Failed to create an SCTask with launch path %@ "
@"because it does not reference an application bundle.", path);
RELEASE(newTask);
return nil;
}
}
else
{
[newTask setLaunchPath: [newTask path]];
}
ASSIGN(newTask->launchIdentity, identity);
newTask->launchPriority = level;
newTask->launchOnDemand = lazily;
newTask->launchOnStart = now;
newTask->hidden = YES;
newTask->stopped = NO;
// NOTE: We could use a better test than just checking whether an 'app'
// extension is present or not...
// pathForApp = [[NSWorkspace sharedWorkspace] fullPathForApplication: [newTask name]];
if ([[[newTask path] pathExtension] isEqual: @"app"])
newTask->isNSApplication = YES;
/* To detect whether an AppKit-based process has been really launched or
not, see -taskLaunched:. */
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver: newTask
selector: @selector(taskLaunched:)
name: NSWorkspaceDidLaunchApplicationNotification
object: nil];
[[NSNotificationCenter defaultCenter] addObserver: newTask
selector: @selector(taskTerminated:)
name: NSTaskDidTerminateNotification
object: newTask];
return [newTask autorelease];
}
/** Returns a new task you can always launch and identical to aTask. The
returned task can be launched even if aTask has already been launched.
This is useful since SCTask as NSTask can be run only one time. */
+ (SCTask *) taskWithTask: (SCTask *)aTask
{
SCTask *newTask = [SCTask taskWithLaunchPath: [aTask launchPath]
priority: [aTask launchPriority]
onStart: [aTask launchOnStart]
onDemand: [aTask launchOnDemand]
withUserName: nil];
[newTask setArguments: [aTask arguments]];
[newTask setCurrentDirectoryPath: [aTask currentDirectoryPath]];
[newTask setEnvironment: [aTask environment]];
[newTask setStandardError: [aTask standardError]];
[newTask setStandardInput: [aTask standardInput]];
[newTask setStandardOutput: [aTask standardOutput]];
newTask->launchFailureCount = aTask->launchFailureCount;
return newTask;
}
- (void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver: self];
TEST_RELEASE(path);
TEST_RELEASE(launchIdentity);
TEST_RELEASE(launchDate);
[super dealloc];
}
- (NSString *) description
{
NSString *desc = [self name];
desc = [desc stringByAppendingFormat: @" %d %@", [self launchPriority],
[super description]];
return desc;
}
- (void) launch
{
NSDebugLLog(@"SCTask", @"Launching task %@", self);
launchDate = [[NSDate alloc] init];
[super launch];
}
- (void) launchForDomain: (NSString *)domain
{
/* At later point, we should check the domain to take in account security.
Domains having usually an associated permissions level. */
[self launch];
stopped = NO;
/* For tools, we notify SSCystem of the launch immediately.
For applications, we waits the application reports that it has finished
to launch before notifying SCSystem.
- If the application launch succeds, we catch NSWorkspaceDidLaunchNotification
with -taskLaunched: and notify System directly.
- If the application launch fails, we catch NSTaskDidTerminateNotification
with -taskTerminated: and notify System directly.
- If the application launch times out, we use a timer to check the time
out delay with -checkLaunchTimeOut, and notify System directly. This
doesn't the launch failed, but the control must return to the launch
queue otherwise the launch sequence can be blocked... */
launchFinished = (isNSApplication == NO); //([self isRunning] && isNSApplication == NO);
if (launchFinished)
{
[self postTaskLaunched];
}
else /* Wait the launch is fully finished by delaying -postTaskLaunched call */
{
NSInvocation *inv = [NSInvocation invocationWithTarget: self
selector: @selector(checkLaunchTimeOut)
arguments: nil];
/* The run loop retains the timer, then releases it once it has fired */
[NSTimer scheduledTimerWithTimeInterval: [self launchTimeOut]
invocation: inv
repeats: NO];
}
}
/** Returns the path used to create the task, it is identical to launch
path most of time, unless the path given initially references an
application bundle and not an executable path directly. */
- (NSString *) path
{
return [[path copy] autorelease];
}
/** Returns the name of the task executable based on -path value. */
- (NSString *) name
{
NSString *name = nil;
NSBundle *bundle = [NSBundle bundleWithPath: [self path]];
NSDictionary *info = [bundle infoDictionary];
name = [info objectForKey: @"ApplicationName"];
if (name == nil)
name = [info objectForKey: @"CFBundleName"];
if (name == nil)
name = [[NSFileManager defaultManager] displayNameAtPath: [bundle executablePath]];
/* A last fallback that should always work */
if (name == nil)
name = [[NSFileManager defaultManager] displayNameAtPath: [self path]];
return name;
}
- (int) launchPriority
{
return launchPriority;
}
- (BOOL) launchOnStart
{
return launchOnStart;
}
- (BOOL) launchOnDemand
{
return launchOnDemand;
}
- (BOOL) isHidden
{
return hidden;
}
- (BOOL) isStopped
{
return stopped;
}
- (void) postTaskLaunched
{
// NOTE: Incompletely filled userInfo dictionary
NSDictionary *userInfo = [NSDictionary
dictionaryWithObjectsAndKeys: [self path], @"NSApplicationPath",
[self name], @"NSApplicationName", nil];
NSNotification *notif = [NSNotification
notificationWithName: NSWorkspaceDidLaunchApplicationNotification
object: self
userInfo: userInfo];
NSDebugLLog(@"SCTask", @"Synthetize launch notification for task %@", self);
// FIXME: We should use -sharedInstance, using -serverInstance prevents
// SCTask API to be used by a third-party application.
[[SCSystem serverInstance] noteApplicationLaunched: notif];
}
/** In case no launch notification is posted or the launch is very slow, we
return the control to the launch queue to be safe.
If GDNC is in trouble, this method will ensure the launch sequence continues
as expected and doesn't get stalled waiting for a
NSWorkspaceDidLaunchApplicationNotification that will never be received. */
- (void) checkLaunchTimeOut
{
/* The time out timer always fires so we have to ignore it if the launch has
succeeded */
if (launchFinished)
return;
// TODO: Try to handle in a more user-friendly manner... may be by asking
// the user for feedback.
NSLog(@"WARNING: Task %@ launch timed out. Any other tasks to be launched "
"that depend on it might not run properly.", [self name]);
NSAssert1(stopped == NO, @"Task %@ is wrongly stopped while it is still "
@"launching", self);
launchFinished = YES;
[self postTaskLaunched];
}
/** We detect when an AppKit-based application has finished to launch with this
method. Each task instance is set up to listen for
NSWorkspaceDidLaunchApplicationNotification. */
- (void) taskLaunched: (NSNotification *)notif
{
NSString *appName = [[notif userInfo] objectForKey: @"NSApplicationName"];
if ([appName isEqual: [self name]] == NO)
return;
NSDebugLLog(@"SCTask", @"Task %@ finished to launch", [self name]);
launchFinished = YES;
[self postTaskLaunched];
}
- (void) taskTerminated: (NSNotification *)notif
{
/* For AppKit-based applications which exits before having finished to
launch, we abuse -postTaskLaunched in order to have them properly removed
from the actual launch queue. */
BOOL wasStillLaunching = (isNSApplication && launchFinished == NO);
stopped = YES;
launchFinished = YES;
if (wasStillLaunching)
{
NSLog(@"WARNING: Failed to launch %@", self);
[self postTaskLaunched];
}
/* We convert launch time to run duration */
// NOTE: -timeIntervalSinceNow returns a negative value in our case,
// therefore we take the absolute value.
runTime = fabs([launchDate timeIntervalSinceNow]);
NSDebugLLog(@"SCTask", @"Task %@ terminated with run interval: %f",
[self name], runTime);
/* We update recent launch failure count */
(runTime < 10) ? launchFailureCount++ : (launchFailureCount = 0);
}
/** Returns the time that elapsed since the launch. If the task is not running
anymore, it returns the run duration (the interval betwen launch and
termination). */
- (NSTimeInterval) runInterval;
{
if ([self isRunning])
{
NSTimeInterval interval = fabs([launchDate timeIntervalSinceNow]);
NSDebugLLog(@"SCTask", @"-runInterval will return: %f", interval);
return interval;
}
else
{
return runTime;
}
}
- (int) launchFailureCount
{
return launchFailureCount;
}
/** Returns the maximal time limit for which the launch queue can be blocked
when a launch is underway. This time limit is set to 5 seconds by default.
If a launch takes longer to finish than the returned time interval, a
warning will be logged, and the control returned to the SCSystem launch
queue that will spawn the next task in the queue. */
- (NSTimeInterval) launchTimeOut
{
return 5.0;
}
/** Returns self but with retain count incremented by one. NSDictionary keys
must conform to NSCopying protocol. This method is implemented to allow
task instances to be used in this role. */
- (id) copyWithZone: (NSZone)zone
{
NSDebugLLog(@"SCTask", @"Copying SCTask %@", self);
return RETAIN(self);
}
@end