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

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