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

Last change on this file since 221 was 221, checked in by rchin, 14 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.