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

Last change on this file since 342 was 342, checked in by rchin, 12 years ago

Leopard compatibility changes:

  • Removed some icon caching code that was causing crashes (not sure we really needed those optimizations anyway -- doesn't appear to affect performance)
  • Added code to automatically try to add certificate to keychain, for new code signing behavior (replaces previous procmod nonesense).
  • Note that the enclosed public certificate is mine, and so it will need to be signed by me. In the case that someone else wants to distribute this binary, please replace Certficiate.cer with your own public certificate, and then make sure o code sign the binary after it is built.
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    }
323}
324
325- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
326{
327    BOOL canInstall = NO;
328   
329    if (row != -1) {
330        DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
331        canInstall = !([patchedApps containsObject: app] ||
332                       [patchingApps containsObject: app]);
333    }
334   
335    [installButton setEnabled: canInstall];
336   
337    return YES;
338}
339
340@end
341
342@implementation FSAAppList (NSTableDataSource)
343
344- (int)numberOfRowsInTableView:(NSTableView *)aTableView
345{
346    return [cocoaApps count];
347}
348
349- (id)tableView:(NSTableView *)aTableView
350    objectValueForTableColumn:(NSTableColumn *)aTableColumn
351            row:(int)rowIndex
352{
353    NSString *columnIdentifier = [aTableColumn identifier];
354   
355    if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
356        return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
357    } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
358        DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
359        if ([patchedApps containsObject: app]) {
360            if (FSACheckMarkImage == nil)
361                return FSACheckMarkCharacter;
362            else
363                return FSACheckMarkImage;
364        }
365        if ([patchingApps containsObject: app]) {
366            if (FSAEllipsisImage == nil)
367                return FSAEllipsisCharacter;
368            else
369                return FSAEllipsisImage;
370        }
371    } else if([columnIdentifier isEqualToString:FSATableColumnIdentifier_always]){
372        if([alwaysApps containsObject:[[cocoaApps objectAtIndex: rowIndex] name]])
373            return [NSNumber numberWithBool:YES];
374        else
375            return [NSNumber numberWithBool:NO];
376    }
377    return nil;
378}
379
380@end
381
382@implementation FSAAppList (NSWindowDelegate)
383
384- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
385{
386    NSWindow *window = [tableView window];
387    NSRect frame = [window frame];
388    NSScrollView *scrollView = [tableView enclosingScrollView];
389    float displayedHeight = [[scrollView contentView] bounds].size.height;
390    float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
391    float heightExcess;
392   
393    if (heightChange >= 0 && heightChange <= 1) {
394        // either the window is already optimal size, or it's too big
395        float rowHeight = [tableView cellHeight];
396        heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
397    }
398   
399    frame.size.height += heightChange;
400   
401    if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
402         (heightExcess = [window maxSize].height - frame.size.height) < 1) {
403        heightChange += heightExcess;
404        frame.size.height += heightExcess;
405    }
406   
407    frame.origin.y -= heightChange;
408   
409    return frame;
410}
411
412@end
413
414@implementation FSAAppList (NSApplicationDelegate)
415
416- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
417{
418    static NSMenu *dockMenu = nil;
419    id<NSMenuItem> menuItem;
420    DeVercruesseProcess *frontApp = [processManager frontProcess];
421    NSString *appName = [frontApp name];
422    NSString *status = nil;
423    // XXX workaround for broken dock menu sender
424    NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
425    NSInvocation *inv;
426   
427    if (dockMenu != nil) {
428        // XXX release invocation
429        [[[dockMenu itemAtIndex: 0] target] release];
430        [dockMenu removeItemAtIndex: 0];
431    } else {
432        dockMenu = [[NSMenu alloc] init];
433    }
434   
435    NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application");
436   
437    if ([patchedApps containsObject: frontApp]) {
438        status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
439    } else if (![cocoaApps containsObject: frontApp]) {
440        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];
441    }
442   
443    if (status == nil) {
444        menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName]
445                                       action: @selector(invoke)
446                                keyEquivalent: @""];
447        inv = [NSInvocation invocationWithMethodSignature: sig];
448        [inv setSelector: @selector(installBundleInFrontmostApp:)];
449        [inv setTarget: NSApp];
450        [inv setArgument: &menuItem atIndex: 2];
451        [menuItem setTag: [frontApp pid]];
452        [menuItem setTarget: [inv retain]];
453    } else {
454        menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
455        [menuItem setEnabled: NO];
456    }
457   
458    return dockMenu;
459}
460
461- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
462{
463    id anApp;
464    id e = [cocoaApps objectEnumerator];
465    while(anApp = [e nextObject]){
466        if([alwaysApps containsObject:[anApp name]]){
467            [NSApp installBundleInAppWithPID:[anApp pid]];
468        }
469    }
470    finishedLaunch = YES;
471}
472
473- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
474{
475    return YES;
476}
477
478-(void)clickInTable:(id)sender
479{
480    if([sender clickedColumn] == 2){
481        if([sender clickedRow] >= 0){
482            NSString *appName = [[cocoaApps objectAtIndex:[sender clickedRow]] name];
483            if(![alwaysApps containsObject:appName]){
484                [NSApp installBundleInAppWithPID:[[cocoaApps objectAtIndex:[sender clickedRow]] pid]];
485                [alwaysApps addObject:appName];
486            } else
487                [alwaysApps removeObject:appName];
488            [[NSUserDefaults standardUserDefaults] setObject:alwaysApps forKey:@"AlwaysApps"];
489        }
490    }
491}
492
493@end
494
495static int sysctlbyname_with_pid (const char *name, pid_t pid,
496                                  void *oldp, size_t *oldlenp,
497                                  void *newp, size_t newlen)
498{
499    if (pid == 0) {
500        if (sysctlbyname(name, oldp, oldlenp, newp, newlen) == -1)  {
501            fprintf(stderr, "sysctlbyname_with_pid(0): sysctlbyname  failed:"
502                    "%s\n", strerror(errno));
503            return -1;
504        }
505    } else {
506        int mib[CTL_MAXNAME];
507        size_t len = CTL_MAXNAME;
508        if (sysctlnametomib(name, mib, &len) == -1) {
509            fprintf(stderr, "sysctlbyname_with_pid: sysctlnametomib  failed:"
510                    "%s\n", strerror(errno));
511            return -1;
512        }
513        mib[len] = pid;
514        len++;
515        if (sysctl(mib, len, oldp, oldlenp, newp, newlen) == -1)  {
516            fprintf(stderr, "sysctlbyname_with_pid: sysctl  failed:"
517                    "%s\n", strerror(errno));
518            return -1;
519        }
520    }
521    return 0;
522}
523
524int is_pid_native (pid_t pid)
525{
526    int ret = 0;
527    size_t sz = sizeof(ret);
528   
529    if (sysctlbyname_with_pid("sysctl.proc_native", pid,
530                              &ret, &sz, NULL, 0) == -1) {
531        if (errno == ENOENT) {
532            // sysctl doesn't exist, which means that this version of Mac OS
533            // pre-dates Rosetta, so the application must be native.
534            return 1;
535        }
536        fprintf(stderr, "is_pid_native: sysctlbyname_with_pid  failed:"
537                "%s\n", strerror(errno));
538        return -1;
539    }
540    return ret;
541}       
Note: See TracBrowser for help on using the repository browser.