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

Last change on this file since 264 was 222, checked in by rchin, 19 years ago

Fixed problem with F-Script leaking file handles (caused F-Script to not be able to inject into applications after a certain number of running apps had been launched). The problem was in appIsPEF calling open(2) but not close(2) before returning.

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 if(app){
146 [patchingApps removeObject: app];
147 [patchedApps addObject: app];
148 }
149 [self _processStatusChanged];
150}
151
152- (void)isPatchingProcessID:(pid_t)pid;
153{
154 [patchingApps addObject: [self _applicationForPID: pid]];
155 [self _processStatusChanged];
156}
157
158- (BOOL)appIsPEF:(DeVercruesseProcess *)app;
159{
160 NSString *bundleExecutableLoc = [app executableLoc];
161 const char *bundleExecutablePath;
162 int fd;
163 PEFContainerHeader pefHeader;
164
165 if (bundleExecutableLoc == NULL)
166 return NO;
167
168 if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
169 return NO;
170
171 if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
172 return NO;
173
174 if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader)){
175 close(fd);
176 return NO;
177 } else
178 close(fd);
179
180 if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
181 return NO;
182
183 return YES;
184}
185
186- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
187{
188 NSString *bundleExecutableLoc = [app executableLoc];
189 if (bundleExecutableLoc == NULL)
190 return NO;
191 return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
192}
193
194-(BOOL)appIsNative:(DeVercruesseProcess *)app
195{
196 return is_pid_native([app pid]);
197}
198
199- (void)addApp:(DeVercruesseProcess *)app;
200{
201 /* Try to determine if the application is a foreground Cocoa application.
202 In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
203 but we don't support such hybrids because the menu items we add depend
204 on Cocoa dispatch mechanisms.
205
206 The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
207 10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
208 are Carbon apps. So we use some code extracted from otool to check
209 whether the application links to the Foundation, AppKit or Cocoa
210 frameworks. This problem is fixed in Jaguar, except that certain CFM
211 Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
212 Conversely, the appIsCocoa: code works on _most_ applications, but
213 Jaguar always correctly identifies Cocoa apps as isCocoa.
214
215 So, our checks go like this:
216 Is the application background-only?
217 Is the application Cocoa or Carbon according to the CPS flavor?
218 If it's Cocoa, is it a CFM app? If so, CPS is lying to us.
219 If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
220 If so, it's really a Cocoa app. If not, it's a Carbon app.
221
222 Be careful not to call appIsCocoa: on a Classic application, you will
223 crash.
224 */
225
226 /*
227 if ([app isCocoa] || [app isCarbon]) {
228 NSLog(@"%@ |%@%@%@%@%@", [app name],
229 [app isBackgroundOnly] ? @" bgOnly" : @"",
230 [app isCocoa] ? @" isCocoa" : @"",
231 [app isCarbon] ? @" isCarbon" : @"",
232 [self appIsPEF: app] ? @" appIsPEF" : @"",
233 [self appIsCocoa: app] ? @" appIsCocoa" : @"");
234 }
235 */
236 if ( ![app isBackgroundOnly] &&
237 ( ( [app isCocoa] && ![self appIsPEF: app]) ||
238 ( [app isCarbon] && [self appIsCocoa: app]))) {
239 if([self appIsNative:app]){
240 [cocoaApps addObject: app];
241 if(finishedLaunch){
242 if([alwaysApps containsObject:[app name]] && ![patchedApps containsObject:app])
243 [NSApp installBundleInAppWithPID:[app pid]];
244 }
245 }
246 }
247 [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
248}
249
250// XXX should insert/resort on launch too; this is harder because of synchronization issues
251
252- (void)update;
253{
254 NSEnumerator *e;
255 NSArray *allApps;
256 DeVercruesseProcess *app;
257
258 [cocoaApps removeAllObjects];
259 [appsByPID removeAllObjects];
260 // [processManager update] unneeded: [processManager processes] sends update
261
262 allApps = [processManager processes];
263 e = [allApps objectEnumerator];
264
265 while ( (app = [e nextObject]) != nil) {
266 [self addApp: app];
267 }
268
269 if([[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]){
270 [[NSUserDefaults standardUserDefaults] synchronize];
271 [alwaysApps removeAllObjects];
272 [alwaysApps addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]];
273 [tableView reloadData];
274 }
275 [tableView noteNumberOfRowsChanged];
276 [self _processStatusChanged];
277}
278
279- (NSArray *)cocoaAppProcessIDs;
280{
281 NSEnumerator *e = [cocoaApps objectEnumerator];
282 NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
283 DeVercruesseProcess *app;
284 while ( (app = [e nextObject]) != nil) {
285 [pids addObject: [NSNumber numberWithInt: [app pid]]];
286 }
287 return pids;
288}
289
290- (void)applicationLaunchedWithProcessID:(pid_t)pid;
291{
292 if ([self _applicationForPID: pid] == nil) {
293 [self update];
294 }
295}
296
297- (void)applicationQuitWithProcessID:(pid_t)pid;
298{
299 DeVercruesseProcess *app = [self _applicationForPID: pid];
300
301 if (app != nil) {
302 [cocoaApps removeObject: app];
303 [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
304 [patchedApps removeObject: app];
305 }
306
307 [tableView noteNumberOfRowsChanged];
308 [self _processStatusChanged];
309}
310
311@end
312
313@implementation FSAAppList (NSTableViewDelegate)
314
315- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
316{
317 if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
318 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
319
320 NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
321 [(NJRLabeledImageCell *)cell setImage: [app img]];
322 [(NJRLabeledImageCell *)cell setImageCacheSource: app];
323 }
324}
325
326- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
327{
328 BOOL canInstall = NO;
329
330 if (row != -1) {
331 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
332 canInstall = !([patchedApps containsObject: app] ||
333 [patchingApps containsObject: app]);
334 }
335
336 [installButton setEnabled: canInstall];
337
338 return YES;
339}
340
341@end
342
343@implementation FSAAppList (NSTableDataSource)
344
345- (int)numberOfRowsInTableView:(NSTableView *)aTableView
346{
347 return [cocoaApps count];
348}
349
350- (id)tableView:(NSTableView *)aTableView
351 objectValueForTableColumn:(NSTableColumn *)aTableColumn
352 row:(int)rowIndex
353{
354 NSString *columnIdentifier = [aTableColumn identifier];
355
356 if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
357 return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
358 } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
359 DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
360 if ([patchedApps containsObject: app]) {
361 if (FSACheckMarkImage == nil)
362 return FSACheckMarkCharacter;
363 else
364 return FSACheckMarkImage;
365 }
366 if ([patchingApps containsObject: app]) {
367 if (FSAEllipsisImage == nil)
368 return FSAEllipsisCharacter;
369 else
370 return FSAEllipsisImage;
371 }
372 } else if([columnIdentifier isEqualToString:FSATableColumnIdentifier_always]){
373 if([alwaysApps containsObject:[[cocoaApps objectAtIndex: rowIndex] name]])
374 return [NSNumber numberWithBool:YES];
375 else
376 return [NSNumber numberWithBool:NO];
377 }
378 return nil;
379}
380
381@end
382
383@implementation FSAAppList (NSWindowDelegate)
384
385- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
386{
387 NSWindow *window = [tableView window];
388 NSRect frame = [window frame];
389 NSScrollView *scrollView = [tableView enclosingScrollView];
390 float displayedHeight = [[scrollView contentView] bounds].size.height;
391 float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
392 float heightExcess;
393
394 if (heightChange >= 0 && heightChange <= 1) {
395 // either the window is already optimal size, or it's too big
396 float rowHeight = [tableView cellHeight];
397 heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
398 }
399
400 frame.size.height += heightChange;
401
402 if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
403 (heightExcess = [window maxSize].height - frame.size.height) < 1) {
404 heightChange += heightExcess;
405 frame.size.height += heightExcess;
406 }
407
408 frame.origin.y -= heightChange;
409
410 return frame;
411}
412
413@end
414
415@implementation FSAAppList (NSApplicationDelegate)
416
417- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
418{
419 static NSMenu *dockMenu = nil;
420 id<NSMenuItem> menuItem;
421 DeVercruesseProcess *frontApp = [processManager frontProcess];
422 NSString *appName = [frontApp name];
423 NSString *status = nil;
424 // XXX workaround for broken dock menu sender
425 NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
426 NSInvocation *inv;
427
428 if (dockMenu != nil) {
429 // XXX release invocation
430 [[[dockMenu itemAtIndex: 0] target] release];
431 [dockMenu removeItemAtIndex: 0];
432 } else {
433 dockMenu = [[NSMenu alloc] init];
434 }
435
436 NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application");
437
438 if ([patchedApps containsObject: frontApp]) {
439 status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
440 } else if (![cocoaApps containsObject: frontApp]) {
441 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];
442 }
443
444 if (status == nil) {
445 menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName]
446 action: @selector(invoke)
447 keyEquivalent: @""];
448 inv = [NSInvocation invocationWithMethodSignature: sig];
449 [inv setSelector: @selector(installBundleInFrontmostApp:)];
450 [inv setTarget: NSApp];
451 [inv setArgument: &menuItem atIndex: 2];
452 [menuItem setTag: [frontApp pid]];
453 [menuItem setTarget: [inv retain]];
454 } else {
455 menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
456 [menuItem setEnabled: NO];
457 }
458
459 return dockMenu;
460}
461
462- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
463{
464 id anApp;
465 id e = [cocoaApps objectEnumerator];
466 while(anApp = [e nextObject]){
467 if([alwaysApps containsObject:[anApp name]]){
468 [NSApp installBundleInAppWithPID:[anApp pid]];
469 }
470 }
471 finishedLaunch = YES;
472}
473
474- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
475{
476 return YES;
477}
478
479-(void)clickInTable:(id)sender
480{
481 if([sender clickedColumn] == 2){
482 if([sender clickedRow] >= 0){
483 NSString *appName = [[cocoaApps objectAtIndex:[sender clickedRow]] name];
484 if(![alwaysApps containsObject:appName]){
485 [NSApp installBundleInAppWithPID:[[cocoaApps objectAtIndex:[sender clickedRow]] pid]];
486 [alwaysApps addObject:appName];
487 } else
488 [alwaysApps removeObject:appName];
489 [[NSUserDefaults standardUserDefaults] setObject:alwaysApps forKey:@"AlwaysApps"];
490 }
491 }
492}
493
494@end
495
496static int sysctlbyname_with_pid (const char *name, pid_t pid,
497 void *oldp, size_t *oldlenp,
498 void *newp, size_t newlen)
499{
500 if (pid == 0) {
501 if (sysctlbyname(name, oldp, oldlenp, newp, newlen) == -1) {
502 fprintf(stderr, "sysctlbyname_with_pid(0): sysctlbyname failed:"
503 "%s\n", strerror(errno));
504 return -1;
505 }
506 } else {
507 int mib[CTL_MAXNAME];
508 size_t len = CTL_MAXNAME;
509 if (sysctlnametomib(name, mib, &len) == -1) {
510 fprintf(stderr, "sysctlbyname_with_pid: sysctlnametomib failed:"
511 "%s\n", strerror(errno));
512 return -1;
513 }
514 mib[len] = pid;
515 len++;
516 if (sysctl(mib, len, oldp, oldlenp, newp, newlen) == -1) {
517 fprintf(stderr, "sysctlbyname_with_pid: sysctl failed:"
518 "%s\n", strerror(errno));
519 return -1;
520 }
521 }
522 return 0;
523}
524
525int is_pid_native (pid_t pid)
526{
527 int ret = 0;
528 size_t sz = sizeof(ret);
529
530 if (sysctlbyname_with_pid("sysctl.proc_native", pid,
531 &ret, &sz, NULL, 0) == -1) {
532 if (errno == ENOENT) {
533 // sysctl doesn't exist, which means that this version of Mac OS
534 // pre-dates Rosetta, so the application must be native.
535 return 1;
536 }
537 fprintf(stderr, "is_pid_native: sysctlbyname_with_pid failed:"
538 "%s\n", strerror(errno));
539 return -1;
540 }
541 return ret;
542}
Note: See TracBrowser for help on using the repository browser.