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

Last change on this file since 630 was 412, checked in by Nicholas Riley, 17 years ago

Fewer warnings and extraneous build settings.

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 "FSAApp.h"
30#import "FSAAppList.h"
31#import "FSAnywhere.h"
32#import "libMatch.h"
33#import "DeVercruesseProcessManager.h"
34#import "NJRLabeledImageCell.h"
35#import "NSTableView-NJRExtensions.h"
36
37// for appIsPEF:
38#import <Carbon/Carbon.h>
39#import <fcntl.h>
40#import <unistd.h>
41
42NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon";
43NSString * const FSATableColumnIdentifier_checkMark = @"checkMark";
44NSString * const FSATableColumnIdentifier_always = @"always";
45
46NSString *FSACheckMarkCharacter;
47NSImage *FSACheckMarkImage;
48
49NSString *FSAEllipsisCharacter;
50NSImage *FSAEllipsisImage;
51
52static const char *FSACocoaFrameworks[] = {
53 "/System/Library/Frameworks/AppKit.framework",
54 "/System/Library/Frameworks/Foundation.framework",
55 "/System/Library/Frameworks/Cocoa.framework",
56 NULL
57};
58
59static int sysctlbyname_with_pid (const char *name, pid_t pid,
60 void *oldp, size_t *oldlenp,
61 void *newp, size_t newlen);
62int is_pid_native (pid_t pid);
63
64@implementation FSAAppList
65
66+ (void)initialize;
67{
68 FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
69 FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"];
70 if (FSACheckMarkImage == nil) {
71 FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]];
72 if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) {
73 [FSACheckMarkImage release];
74 FSACheckMarkImage = nil;
75 }
76 FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage);
77 }
78 FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1];
79 FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]];
80 if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) {
81 [FSAEllipsisImage release];
82 FSAEllipsisImage = nil;
83 }
84}
85
86- (void)awakeFromNib;
87{
88 NSWindow *window = [tableView window];
89
90 processManager = [DeVercruesseProcessManager defaultManager];
91 cocoaApps = [[NSMutableArray alloc] init];
92 patchedApps = [[NSMutableSet alloc] init];
93 patchingApps = [[NSMutableSet alloc] init];
94 appsByPID = [[NSMutableDictionary alloc] init];
95 alwaysApps = [[NSMutableArray array] retain];
96
97 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
98 setDataCell: [NJRLabeledImageCell cell]];
99 if (FSACheckMarkImage != nil)
100 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark]
101 setDataCell: [[[NSImageCell alloc] init] autorelease]];
102 [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];
103 [tableView setTarget:self];
104 [tableView setAction:@selector(clickInTable:)];
105
106 [self update];
107 [window makeFirstResponder: tableView];
108
109 [[NSNotificationCenter defaultCenter] addObserver:self
110 selector:@selector(update)
111 name:NSUserDefaultsDidChangeNotification
112 object:nil];
113}
114
115- (void)dealloc;
116{
117 // don't release processManager, we don't own it
118 [cocoaApps release];
119 [patchedApps release];
120 [appsByPID release];
121 [super dealloc];
122}
123
124- (pid_t)selectedProcessID;
125{
126 int row = [tableView selectedRow];
127 if (row == -1) return -1;
128
129 return [[cocoaApps objectAtIndex: row] pid];
130}
131
132- (void)_processStatusChanged;
133{
134 [tableView reloadData];
135// [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
136}
137
138- (DeVercruesseProcess *)_applicationForPID:(pid_t)pid;
139{
140 return [appsByPID objectForKey: [NSNumber numberWithInt: pid]];
141}
142
143- (void)didPatchProcessID:(pid_t)pid;
144{
145 DeVercruesseProcess *app = [self _applicationForPID: pid];
146 if(app){
147 [patchingApps removeObject: app];
148 [patchedApps addObject: app];
149 }
150 [self _processStatusChanged];
151}
152
153- (void)isPatchingProcessID:(pid_t)pid;
154{
155 [patchingApps addObject: [self _applicationForPID: pid]];
156 [self _processStatusChanged];
157}
158
159- (BOOL)appIsPEF:(DeVercruesseProcess *)app;
160{
161 NSString *bundleExecutableLoc = [app executableLoc];
162 const char *bundleExecutablePath;
163 int fd;
164 PEFContainerHeader pefHeader;
165
166 if (bundleExecutableLoc == NULL)
167 return NO;
168
169 if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
170 return NO;
171
172 if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
173 return NO;
174
175 if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader)){
176 close(fd);
177 return NO;
178 } else
179 close(fd);
180
181 if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
182 return NO;
183
184 return YES;
185}
186
187- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
188{
189 NSString *bundleExecutableLoc = [app executableLoc];
190 if (bundleExecutableLoc == NULL)
191 return NO;
192 return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
193}
194
195-(BOOL)appIsNative:(DeVercruesseProcess *)app
196{
197 return is_pid_native([app pid]);
198}
199
200- (void)addApp:(DeVercruesseProcess *)app;
201{
202 /* Try to determine if the application is a foreground Cocoa application.
203 In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
204 but we don't support such hybrids because the menu items we add depend
205 on Cocoa dispatch mechanisms.
206
207 The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
208 10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
209 are Carbon apps. So we use some code extracted from otool to check
210 whether the application links to the Foundation, AppKit or Cocoa
211 frameworks. This problem is fixed in Jaguar, except that certain CFM
212 Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
213 Conversely, the appIsCocoa: code works on _most_ applications, but
214 Jaguar always correctly identifies Cocoa apps as isCocoa.
215
216 So, our checks go like this:
217 Is the application background-only?
218 Is the application Cocoa or Carbon according to the CPS flavor?
219 If it's Cocoa, is it a CFM app? If so, CPS is lying to us.
220 If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
221 If so, it's really a Cocoa app. If not, it's a Carbon app.
222
223 Be careful not to call appIsCocoa: on a Classic application, you will
224 crash.
225 */
226
227 /*
228 if ([app isCocoa] || [app isCarbon]) {
229 NSLog(@"%@ |%@%@%@%@%@", [app name],
230 [app isBackgroundOnly] ? @" bgOnly" : @"",
231 [app isCocoa] ? @" isCocoa" : @"",
232 [app isCarbon] ? @" isCarbon" : @"",
233 [self appIsPEF: app] ? @" appIsPEF" : @"",
234 [self appIsCocoa: app] ? @" appIsCocoa" : @"");
235 }
236 */
237 if ( ![app isBackgroundOnly] &&
238 ( ( [app isCocoa] && ![self appIsPEF: app]) ||
239 ( [app isCarbon] && [self appIsCocoa: app]))) {
240 if([self appIsNative:app]){
241 [cocoaApps addObject: app];
242 if(finishedLaunch){
243 if([alwaysApps containsObject:[app name]] && ![patchedApps containsObject:app])
244 [(FSAApp *)NSApp installBundleInAppWithPID:[app pid]];
245 }
246 }
247 }
248 [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
249}
250
251// XXX should insert/resort on launch too; this is harder because of synchronization issues
252
253- (void)update;
254{
255 NSEnumerator *e;
256 NSArray *allApps;
257 DeVercruesseProcess *app;
258
259 [cocoaApps removeAllObjects];
260 [appsByPID removeAllObjects];
261 // [processManager update] unneeded: [processManager processes] sends update
262
263 allApps = [processManager processes];
264 e = [allApps objectEnumerator];
265
266 while ( (app = [e nextObject]) != nil) {
267 [self addApp: app];
268 }
269
270 if([[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]){
271 [[NSUserDefaults standardUserDefaults] synchronize];
272 [alwaysApps removeAllObjects];
273 [alwaysApps addObjectsFromArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"AlwaysApps"]];
274 [tableView reloadData];
275 }
276 [tableView noteNumberOfRowsChanged];
277 [self _processStatusChanged];
278}
279
280- (NSArray *)cocoaAppProcessIDs;
281{
282 NSEnumerator *e = [cocoaApps objectEnumerator];
283 NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
284 DeVercruesseProcess *app;
285 while ( (app = [e nextObject]) != nil) {
286 [pids addObject: [NSNumber numberWithInt: [app pid]]];
287 }
288 return pids;
289}
290
291- (void)applicationLaunchedWithProcessID:(pid_t)pid;
292{
293 if ([self _applicationForPID: pid] == nil) {
294 [self update];
295 }
296}
297
298- (void)applicationQuitWithProcessID:(pid_t)pid;
299{
300 DeVercruesseProcess *app = [self _applicationForPID: pid];
301
302 if (app != nil) {
303 [cocoaApps removeObject: app];
304 [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
305 [patchedApps removeObject: app];
306 }
307
308 [tableView noteNumberOfRowsChanged];
309 [self _processStatusChanged];
310}
311
312@end
313
314@implementation FSAAppList (NSTableViewDelegate)
315
316- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
317{
318 if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
319 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
320
321 NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
322 [(NJRLabeledImageCell *)cell setImage: [app img]];
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 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 [(FSAApp *)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 [(FSAApp *)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.