source: trunk/Cocoa/F-Script Anywhere/Source/FSAAppList.m@ 221

Last change on this file since 221 was 221, checked in by rchin, 18 years ago

We now have a preferences dialog box to remove auto-injected apps, incase the crashes on auto-inject (which would make it difficult to untick the proper box).

File size: 17.4 KB
Line 
1//
2// FSAAppList.m
3// F-Script Anywhere
4//
5// Created by Nicholas Riley on Fri Feb 01 2002.
6// Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9/*
10
11 F-Script Anywhere is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2 of the License, or
14 (at your option) any later version.
15
16 F-Script Anywhere is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with F-Script Anywhere; if not, write to the Free Software
23 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24
25 */
26
27#include <sys/types.h>
28#include <sys/sysctl.h>
29#import "FSAAppList.h"
30#import "FSAnywhere.h"
31#import "libMatch.h"
32#import "DeVercruesseProcessManager.h"
33#import "NJRLabeledImageCell.h"
34#import "NSTableView-NJRExtensions.h"
35
36// for appIsPEF:
37#import <Carbon/Carbon.h>
38#import <fcntl.h>
39#import <unistd.h>
40
41NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon";
42NSString * const FSATableColumnIdentifier_checkMark = @"checkMark";
43NSString * const FSATableColumnIdentifier_always = @"always";
44
45NSString *FSACheckMarkCharacter;
46NSImage *FSACheckMarkImage;
47
48NSString *FSAEllipsisCharacter;
49NSImage *FSAEllipsisImage;
50
51static const char *FSACocoaFrameworks[] = {
52 "/System/Library/Frameworks/AppKit.framework",
53 "/System/Library/Frameworks/Foundation.framework",
54 "/System/Library/Frameworks/Cocoa.framework",
55 NULL
56};
57
58static int sysctlbyname_with_pid (const char *name, pid_t pid,
59 void *oldp, size_t *oldlenp,
60 void *newp, size_t newlen);
61int is_pid_native (pid_t pid);
62
63@implementation FSAAppList
64
65+ (void)initialize;
66{
67 FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
68 FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"];
69 if (FSACheckMarkImage == nil) {
70 FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]];
71 if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) {
72 [FSACheckMarkImage release];
73 FSACheckMarkImage = nil;
74 }
75 FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage);
76 }
77 FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1];
78 FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]];
79 if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) {
80 [FSAEllipsisImage release];
81 FSAEllipsisImage = nil;
82 }
83}
84
85- (void)awakeFromNib;
86{
87 NSWindow *window = [tableView window];
88
89 processManager = [DeVercruesseProcessManager defaultManager];
90 cocoaApps = [[NSMutableArray alloc] init];
91 patchedApps = [[NSMutableSet alloc] init];
92 patchingApps = [[NSMutableSet alloc] init];
93 appsByPID = [[NSMutableDictionary alloc] init];
94 alwaysApps = [[NSMutableArray array] retain];
95
96 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
97 setDataCell: [NJRLabeledImageCell cell]];
98 if (FSACheckMarkImage != nil)
99 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark]
100 setDataCell: [[[NSImageCell alloc] init] autorelease]];
101 [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];
102 [tableView setTarget:self];
103 [tableView setAction:@selector(clickInTable:)];
104
105 [self update];
106 [window makeFirstResponder: tableView];
107
108 [[NSNotificationCenter defaultCenter] addObserver:self
109 selector:@selector(update)
110 name:NSUserDefaultsDidChangeNotification
111 object:nil];
112}
113
114- (void)dealloc;
115{
116 // don't release processManager, we don't own it
117 [cocoaApps release];
118 [patchedApps release];
119 [appsByPID release];
120 [super dealloc];
121}
122
123- (pid_t)selectedProcessID;
124{
125 int row = [tableView selectedRow];
126 if (row == -1) return -1;
127
128 return [[cocoaApps objectAtIndex: row] pid];
129}
130
131- (void)_processStatusChanged;
132{
133 [tableView reloadData];
134 [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
135}
136
137- (DeVercruesseProcess *)_applicationForPID:(pid_t)pid;
138{
139 return [appsByPID objectForKey: [NSNumber numberWithInt: pid]];
140}
141
142- (void)didPatchProcessID:(pid_t)pid;
143{
144 DeVercruesseProcess *app = [self _applicationForPID: pid];
145 [patchingApps removeObject: app];
146 [patchedApps addObject: app];
147 [self _processStatusChanged];
148}
149
150- (void)isPatchingProcessID:(pid_t)pid;
151{
152 [patchingApps addObject: [self _applicationForPID: pid]];
153 [self _processStatusChanged];
154}
155
156- (BOOL)appIsPEF:(DeVercruesseProcess *)app;
157{
158 NSString *bundleExecutableLoc = [app executableLoc];
159 const char *bundleExecutablePath;
160 int fd;
161 PEFContainerHeader pefHeader;
162
163 if (bundleExecutableLoc == NULL)
164 return NO;
165
166 if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
167 return NO;
168
169 if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
170 return NO;
171
172 if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader))
173 return NO;
174
175 if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
176 return NO;
177
178 return YES;
179}
180
181- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
182{
183 NSString *bundleExecutableLoc = [app executableLoc];
184 if (bundleExecutableLoc == NULL)
185 return NO;
186 return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
187}
188
189-(BOOL)appIsNative:(DeVercruesseProcess *)app
190{
191 return is_pid_native([app pid]);
192}
193
194- (void)addApp:(DeVercruesseProcess *)app;
195{
196 /* Try to determine if the application is a foreground Cocoa application.
197 In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
198 but we don't support such hybrids because the menu items we add depend
199 on Cocoa dispatch mechanisms.
200
201 The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
202 10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
203 are Carbon apps. So we use some code extracted from otool to check
204 whether the application links to the Foundation, AppKit or Cocoa
205 frameworks. This problem is fixed in Jaguar, except that certain CFM
206 Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
207 Conversely, the appIsCocoa: code works on _most_ applications, but
208 Jaguar always correctly identifies Cocoa apps as isCocoa.
209
210 So, our checks go like this:
211 Is the application background-only?
212 Is the application Cocoa or Carbon according to the CPS flavor?
213 If it's Cocoa, is it a CFM app? If so, CPS is lying to us.
214 If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
215 If so, it's really a Cocoa app. If not, it's a Carbon app.
216
217 Be careful not to call appIsCocoa: on a Classic application, you will
218 crash.
219 */
220
221 /*
222 if ([app isCocoa] || [app isCarbon]) {
223 NSLog(@"%@ |%@%@%@%@%@", [app name],
224 [app isBackgroundOnly] ? @" bgOnly" : @"",
225 [app isCocoa] ? @" isCocoa" : @"",
226 [app isCarbon] ? @" isCarbon" : @"",
227 [self appIsPEF: app] ? @" appIsPEF" : @"",
228 [self appIsCocoa: app] ? @" appIsCocoa" : @"");
229 }
230 */
231
232 if ( ![app isBackgroundOnly] &&
233 ( ( [app isCocoa] && ![self appIsPEF: app]) ||
234 ( [app isCarbon] && [self appIsCocoa: app]))) {
235 if([self appIsNative:app]){
236 [cocoaApps addObject: app];
237 if(finishedLaunch){
238 if([alwaysApps containsObject:[app name]] && ![patchedApps containsObject:app])
239 [NSApp installBundleInAppWithPID:[app pid]];
240 }
241 }
242 }
243 [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
244}
245
246// XXX should insert/resort on launch too; this is harder because of synchronization issues
247
248- (void)update;
249{
250 NSEnumerator *e;
251 NSArray *allApps;
252 DeVercruesseProcess *app;
253
254 [cocoaApps removeAllObjects];
255 [appsByPID removeAllObjects];
256 // [processManager update] unneeded: [processManager processes] sends update
257
258 allApps = [processManager processes];
259 e = [allApps objectEnumerator];
260
261 while ( (app = [e nextObject]) != nil) {
262 [self addApp: app];
263 }
264
265 if([[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]){
266 [[NSUserDefaults standardUserDefaults] synchronize];
267 [alwaysApps removeAllObjects];
268 [alwaysApps addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]];
269 [tableView reloadData];
270 }
271 [tableView noteNumberOfRowsChanged];
272 [self _processStatusChanged];
273}
274
275- (NSArray *)cocoaAppProcessIDs;
276{
277 NSEnumerator *e = [cocoaApps objectEnumerator];
278 NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
279 DeVercruesseProcess *app;
280 while ( (app = [e nextObject]) != nil) {
281 [pids addObject: [NSNumber numberWithInt: [app pid]]];
282 }
283 return pids;
284}
285
286- (void)applicationLaunchedWithProcessID:(pid_t)pid;
287{
288 if ([self _applicationForPID: pid] == nil) {
289 [self update];
290 }
291}
292
293- (void)applicationQuitWithProcessID:(pid_t)pid;
294{
295 DeVercruesseProcess *app = [self _applicationForPID: pid];
296
297 if (app != nil) {
298 [cocoaApps removeObject: app];
299 [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
300 [patchedApps removeObject: app];
301 }
302
303 [tableView noteNumberOfRowsChanged];
304 [self _processStatusChanged];
305}
306
307@end
308
309@implementation FSAAppList (NSTableViewDelegate)
310
311- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
312{
313 if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
314 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
315
316 NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
317 [(NJRLabeledImageCell *)cell setImage: [app img]];
318 [(NJRLabeledImageCell *)cell setImageCacheSource: app];
319 }
320}
321
322- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
323{
324 BOOL canInstall = NO;
325
326 if (row != -1) {
327 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
328 canInstall = !([patchedApps containsObject: app] ||
329 [patchingApps containsObject: app]);
330 }
331
332 [installButton setEnabled: canInstall];
333
334 return YES;
335}
336
337@end
338
339@implementation FSAAppList (NSTableDataSource)
340
341- (int)numberOfRowsInTableView:(NSTableView *)aTableView
342{
343 return [cocoaApps count];
344}
345
346- (id)tableView:(NSTableView *)aTableView
347 objectValueForTableColumn:(NSTableColumn *)aTableColumn
348 row:(int)rowIndex
349{
350 NSString *columnIdentifier = [aTableColumn identifier];
351
352 if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
353 return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
354 } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
355 DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
356 if ([patchedApps containsObject: app]) {
357 if (FSACheckMarkImage == nil)
358 return FSACheckMarkCharacter;
359 else
360 return FSACheckMarkImage;
361 }
362 if ([patchingApps containsObject: app]) {
363 if (FSAEllipsisImage == nil)
364 return FSAEllipsisCharacter;
365 else
366 return FSAEllipsisImage;
367 }
368 } else if([columnIdentifier isEqualToString:FSATableColumnIdentifier_always]){
369 if([alwaysApps containsObject:[[cocoaApps objectAtIndex: rowIndex] name]])
370 return [NSNumber numberWithBool:YES];
371 else
372 return [NSNumber numberWithBool:NO];
373 }
374 return nil;
375}
376
377@end
378
379@implementation FSAAppList (NSWindowDelegate)
380
381- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
382{
383 NSWindow *window = [tableView window];
384 NSRect frame = [window frame];
385 NSScrollView *scrollView = [tableView enclosingScrollView];
386 float displayedHeight = [[scrollView contentView] bounds].size.height;
387 float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
388 float heightExcess;
389
390 if (heightChange >= 0 && heightChange <= 1) {
391 // either the window is already optimal size, or it's too big
392 float rowHeight = [tableView cellHeight];
393 heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
394 }
395
396 frame.size.height += heightChange;
397
398 if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
399 (heightExcess = [window maxSize].height - frame.size.height) < 1) {
400 heightChange += heightExcess;
401 frame.size.height += heightExcess;
402 }
403
404 frame.origin.y -= heightChange;
405
406 return frame;
407}
408
409@end
410
411@implementation FSAAppList (NSApplicationDelegate)
412
413- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
414{
415 static NSMenu *dockMenu = nil;
416 id<NSMenuItem> menuItem;
417 DeVercruesseProcess *frontApp = [processManager frontProcess];
418 NSString *appName = [frontApp name];
419 NSString *status = nil;
420 // XXX workaround for broken dock menu sender
421 NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
422 NSInvocation *inv;
423
424 if (dockMenu != nil) {
425 // XXX release invocation
426 [[[dockMenu itemAtIndex: 0] target] release];
427 [dockMenu removeItemAtIndex: 0];
428 } else {
429 dockMenu = [[NSMenu alloc] init];
430 }
431
432 NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application");
433
434 if ([patchedApps containsObject: frontApp]) {
435 status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
436 } else if (![cocoaApps containsObject: frontApp]) {
437 status = [NSString stringWithFormat: NSLocalizedString(@"Can't install because '%@' is not a Cocoa application", "Dock menu disabled item displayed when frontmost app not Cocoa, app name parameter"), appName];
438 }
439
440 if (status == nil) {
441 menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName]
442 action: @selector(invoke)
443 keyEquivalent: @""];
444 inv = [NSInvocation invocationWithMethodSignature: sig];
445 [inv setSelector: @selector(installBundleInFrontmostApp:)];
446 [inv setTarget: NSApp];
447 [inv setArgument: &menuItem atIndex: 2];
448 [menuItem setTag: [frontApp pid]];
449 [menuItem setTarget: [inv retain]];
450 } else {
451 menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
452 [menuItem setEnabled: NO];
453 }
454
455 return dockMenu;
456}
457
458- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
459{
460 id anApp;
461 id e = [cocoaApps objectEnumerator];
462 while(anApp = [e nextObject]){
463 if([alwaysApps containsObject:[anApp name]] && ![patchedApps containsObject:anApp]){
464 [NSApp installBundleInAppWithPID:[anApp pid]];
465 }
466 }
467 finishedLaunch = YES;
468}
469
470- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
471{
472 return YES;
473}
474
475-(void)clickInTable:(id)sender
476{
477 if([sender clickedColumn] == 2){
478 if([sender clickedRow] >= 0){
479 NSString *appName = [[cocoaApps objectAtIndex:[sender clickedRow]] name];
480 if(![alwaysApps containsObject:appName]){
481 [NSApp installBundleInAppWithPID:[[cocoaApps objectAtIndex:[sender clickedRow]] pid]];
482 [alwaysApps addObject:appName];
483 } else
484 [alwaysApps removeObject:appName];
485 [[NSUserDefaults standardUserDefaults] setObject:alwaysApps forKey:@"AlwaysApps"];
486 }
487 }
488}
489
490@end
491
492static int sysctlbyname_with_pid (const char *name, pid_t pid,
493 void *oldp, size_t *oldlenp,
494 void *newp, size_t newlen)
495{
496 if (pid == 0) {
497 if (sysctlbyname(name, oldp, oldlenp, newp, newlen) == -1) {
498 fprintf(stderr, "sysctlbyname_with_pid(0): sysctlbyname failed:"
499 "%s\n", strerror(errno));
500 return -1;
501 }
502 } else {
503 int mib[CTL_MAXNAME];
504 size_t len = CTL_MAXNAME;
505 if (sysctlnametomib(name, mib, &len) == -1) {
506 fprintf(stderr, "sysctlbyname_with_pid: sysctlnametomib failed:"
507 "%s\n", strerror(errno));
508 return -1;
509 }
510 mib[len] = pid;
511 len++;
512 if (sysctl(mib, len, oldp, oldlenp, newp, newlen) == -1) {
513 fprintf(stderr, "sysctlbyname_with_pid: sysctl failed:"
514 "%s\n", strerror(errno));
515 return -1;
516 }
517 }
518 return 0;
519}
520
521int is_pid_native (pid_t pid)
522{
523 int ret = 0;
524 size_t sz = sizeof(ret);
525
526 if (sysctlbyname_with_pid("sysctl.proc_native", pid,
527 &ret, &sz, NULL, 0) == -1) {
528 if (errno == ENOENT) {
529 // sysctl doesn't exist, which means that this version of Mac OS
530 // pre-dates Rosetta, so the application must be native.
531 return 1;
532 }
533 fprintf(stderr, "is_pid_native: sysctlbyname_with_pid failed:"
534 "%s\n", strerror(errno));
535 return -1;
536 }
537 return ret;
538}
Note: See TracBrowser for help on using the repository browser.