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

Last change on this file since 412 was 412, checked in by Nicholas Riley, 12 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.