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

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

Fixed commit of nib file.

Added support for automatic injection into apps.

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