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

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