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

Last change on this file since 153 was 153, checked in by Nicholas Riley, 20 years ago

Integrates SCPatch and mach_inject; unfinished, buggy.

File size: 13.8 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#import "FSAAppList.h"
28#import "FSAnywhere.h"
29#import "libMatch.h"
30#import "DeVercruesseProcessManager.h"
31#import "NJRLabeledImageCell.h"
32#import "NSTableView-NJRExtensions.h"
33
34// for appIsPEF:
35#import <Carbon/Carbon.h>
36#import <fcntl.h>
37#import <unistd.h>
38
39NSString * const FSATableColumnIdentifier_appNameAndIcon = @"appNameAndIcon";
40NSString * const FSATableColumnIdentifier_checkMark = @"checkMark";
41
42NSString *FSACheckMarkCharacter;
43NSImage *FSACheckMarkImage;
44
45NSString *FSAEllipsisCharacter;
46NSImage *FSAEllipsisImage;
47
48static const char *FSACocoaFrameworks[] = {
49 "/System/Library/Frameworks/AppKit.framework",
50 "/System/Library/Frameworks/Foundation.framework",
51 "/System/Library/Frameworks/Cocoa.framework",
52 NULL
53};
54
55@implementation FSAAppList
56
57+ (void)initialize;
58{
59 FSACheckMarkCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x27\x13" length: 1];
60 FSACheckMarkImage = [NSImage imageNamed: @"NSMenuCheckmark"];
61 if (FSACheckMarkImage == nil) {
62 FSACheckMarkImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Fallback checkmark" ofType: @"tiff"]];
63 if (FSACheckMarkImage != nil && ![FSACheckMarkImage isValid]) {
64 [FSACheckMarkImage release];
65 FSACheckMarkImage = nil;
66 }
67 FSALog(@"Falling back to checkmark image from bundle: %@", FSACheckMarkImage);
68 }
69 FSAEllipsisCharacter = [[NSString alloc] initWithCharacters: (const unichar *)"\x20\x26" length: 1];
70 FSAEllipsisImage = [[NSImage alloc] initByReferencingFile: [[NSBundle mainBundle] pathForResource: @"Ellipsis" ofType: @"tiff"]];
71 if (FSAEllipsisImage != nil && ![FSAEllipsisImage isValid]) {
72 [FSAEllipsisImage release];
73 FSAEllipsisImage = nil;
74 }
75}
76
77- (void)awakeFromNib;
78{
79 NSWindow *window = [tableView window];
80
81 processManager = [DeVercruesseProcessManager defaultManager];
82 cocoaApps = [[NSMutableArray alloc] init];
83 patchedApps = [[NSMutableSet alloc] init];
84 patchingApps = [[NSMutableSet alloc] init];
85 appsByPID = [[NSMutableDictionary alloc] init];
86
87 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_appNameAndIcon]
88 setDataCell: [NJRLabeledImageCell cell]];
89 if (FSACheckMarkImage != nil)
90 [[tableView tableColumnWithIdentifier: FSATableColumnIdentifier_checkMark]
91 setDataCell: [[[NSImageCell alloc] init] autorelease]];
92 [window setResizeIncrements: NSMakeSize(1, [tableView cellHeight])];
93
94 [self update];
95 [window makeFirstResponder: tableView];
96}
97
98- (void)dealloc;
99{
100 // don't release processManager, we don't own it
101 [cocoaApps release];
102 [patchedApps release];
103 [appsByPID release];
104 [super dealloc];
105}
106
107- (pid_t)selectedProcessID;
108{
109 int row = [tableView selectedRow];
110 if (row == -1) return -1;
111
112 return [[cocoaApps objectAtIndex: row] pid];
113}
114
115- (void)_processStatusChanged;
116{
117 [tableView reloadData];
118 [self tableView: tableView shouldSelectRow: [tableView selectedRow]];
119}
120
121- (DeVercruesseProcess *)_applicationForPID:(pid_t)pid;
122{
123 return [appsByPID objectForKey: [NSNumber numberWithInt: pid]];
124}
125
126- (void)didPatchProcessID:(pid_t)pid;
127{
128 DeVercruesseProcess *app = [self _applicationForPID: pid];
129 [patchingApps removeObject: app];
130 [patchedApps addObject: app];
131 [self _processStatusChanged];
132}
133
134- (void)isPatchingProcessID:(pid_t)pid;
135{
136 [patchingApps addObject: [self _applicationForPID: pid]];
137 [self _processStatusChanged];
138}
139
140- (BOOL)appIsPEF:(DeVercruesseProcess *)app;
141{
142 NSString *bundleExecutableLoc = [app executableLoc];
143 const char *bundleExecutablePath;
144 int fd;
145 PEFContainerHeader pefHeader;
146
147 if (bundleExecutableLoc == NULL)
148 return NO;
149
150 if ( (bundleExecutablePath = [bundleExecutableLoc fileSystemRepresentation]) == NULL)
151 return NO;
152
153 if ( (fd = open(bundleExecutablePath, O_RDONLY, 0)) == -1)
154 return NO;
155
156 if (read(fd, &pefHeader, sizeof(pefHeader)) != sizeof(pefHeader))
157 return NO;
158
159 if (pefHeader.tag1 != kPEFTag1 || pefHeader.tag2 != kPEFTag2)
160 return NO;
161
162 return YES;
163}
164
165- (BOOL)appIsCocoa:(DeVercruesseProcess *)app;
166{
167 NSString *bundleExecutableLoc = [app executableLoc];
168 if (bundleExecutableLoc == NULL)
169 return NO;
170 return appContainsLibMatching([bundleExecutableLoc fileSystemRepresentation], FSACocoaFrameworks);
171}
172
173- (void)addApp:(DeVercruesseProcess *)app;
174{
175 /* Try to determine if the application is a foreground Cocoa application.
176 In Jaguar, itÕs possible to mix Cocoa in a primarily Carbon application,
177 but we don't support such hybrids because the menu items we add depend
178 on Cocoa dispatch mechanisms.
179
180 The CPS 'flavor' mechanism (isCarbon, isCocoa) is broken in Mac OS X
181 10.1.2 through 10.1.5 and possibly earlier, reporting that all Cocoa apps
182 are Carbon apps. So we use some code extracted from otool to check
183 whether the application links to the Foundation, AppKit or Cocoa
184 frameworks. This problem is fixed in Jaguar, except that certain CFM
185 Carbon apps are reported to be Cocoa apps (Drop Drawers is one example).
186 Conversely, the appIsCocoa: code works on _most_ applications, but
187 Jaguar always correctly identifies Cocoa apps as isCocoa.
188
189 So, our checks go like this:
190 Is the application background-only?
191 Is the application Cocoa or Carbon according to the CPS flavor?
192 If it's Cocoa, is it a CFM app? If so, CPS is lying to us.
193 If it's "Carbon", does it link to AppKit, Foundation or Cocoa?
194 If so, it's really a Cocoa app. If not, it's a Carbon app.
195
196 Be careful not to call appIsCocoa: on a Classic application, you will
197 crash.
198 */
199
200 /*
201 if ([app isCocoa] || [app isCarbon]) {
202 NSLog(@"%@ |%@%@%@%@%@", [app name],
203 [app isBackgroundOnly] ? @" bgOnly" : @"",
204 [app isCocoa] ? @" isCocoa" : @"",
205 [app isCarbon] ? @" isCarbon" : @"",
206 [self appIsPEF: app] ? @" appIsPEF" : @"",
207 [self appIsCocoa: app] ? @" appIsCocoa" : @"");
208 }
209 */
210
211 if ( ![app isBackgroundOnly] &&
212 ( ( [app isCocoa] && ![self appIsPEF: app]) ||
213 ( [app isCarbon] && [self appIsCocoa: app]))) {
214 [cocoaApps addObject: app];
215 }
216 [appsByPID setObject: app forKey: [NSNumber numberWithInt: [app pid]]];
217}
218
219// XXX should insert/resort on launch too; this is harder because of synchronization issues
220
221- (void)update;
222{
223 NSEnumerator *e;
224 NSArray *allApps;
225 DeVercruesseProcess *app;
226
227 [cocoaApps removeAllObjects];
228 [appsByPID removeAllObjects];
229 // [processManager update] unneeded: [processManager processes] sends update
230
231 allApps = [processManager processes];
232 e = [allApps objectEnumerator];
233
234 while ( (app = [e nextObject]) != nil) {
235 [self addApp: app];
236 }
237
238 [tableView noteNumberOfRowsChanged];
239 [self _processStatusChanged];
240}
241
242- (NSArray *)cocoaAppProcessIDs;
243{
244 NSEnumerator *e = [cocoaApps objectEnumerator];
245 NSMutableArray *pids = [NSMutableArray arrayWithCapacity: [cocoaApps count]];
246 DeVercruesseProcess *app;
247 while ( (app = [e nextObject]) != nil) {
248 [pids addObject: [NSNumber numberWithInt: [app pid]]];
249 }
250 return pids;
251}
252
253- (void)applicationLaunchedWithProcessID:(pid_t)pid;
254{
255 if ([self _applicationForPID: pid] == nil) {
256 [self update];
257 }
258}
259
260- (void)applicationQuitWithProcessID:(pid_t)pid;
261{
262 DeVercruesseProcess *app = [self _applicationForPID: pid];
263
264 if (app != nil) {
265 [cocoaApps removeObject: app];
266 [appsByPID removeObjectForKey: [NSNumber numberWithLong: pid]];
267 [patchedApps removeObject: app];
268 }
269
270 [tableView noteNumberOfRowsChanged];
271 [self _processStatusChanged];
272}
273
274@end
275
276@implementation FSAAppList (NSTableViewDelegate)
277
278- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row;
279{
280 if ([[tableColumn identifier] isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
281 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
282
283 NSAssert1([cell isKindOfClass: [NJRLabeledImageCell class]], @"Cell is not what we expected, instead %@", cell);
284 [(NJRLabeledImageCell *)cell setImage: [app img]];
285 [(NJRLabeledImageCell *)cell setImageCacheSource: app];
286 }
287}
288
289- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
290{
291 BOOL canInstall = NO;
292
293 if (row != -1) {
294 DeVercruesseProcess *app = [cocoaApps objectAtIndex: row];
295 canInstall = !([patchedApps containsObject: app] ||
296 [patchingApps containsObject: app]);
297 }
298
299 [installButton setEnabled: canInstall];
300
301 return YES;
302}
303
304@end
305
306@implementation FSAAppList (NSTableDataSource)
307
308- (int)numberOfRowsInTableView:(NSTableView *)aTableView
309{
310 return [cocoaApps count];
311}
312
313- (id)tableView:(NSTableView *)aTableView
314 objectValueForTableColumn:(NSTableColumn *)aTableColumn
315 row:(int)rowIndex
316{
317 NSString *columnIdentifier = [aTableColumn identifier];
318
319 if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_appNameAndIcon]) {
320 return [(DeVercruesseProcess *)[cocoaApps objectAtIndex: rowIndex] name];
321 } else if ([columnIdentifier isEqualToString: FSATableColumnIdentifier_checkMark]) {
322 DeVercruesseProcess *app = [cocoaApps objectAtIndex: rowIndex];
323 if ([patchedApps containsObject: app]) {
324 if (FSACheckMarkImage == nil)
325 return FSACheckMarkCharacter;
326 else
327 return FSACheckMarkImage;
328 }
329 if ([patchingApps containsObject: app]) {
330 if (FSAEllipsisImage == nil)
331 return FSAEllipsisCharacter;
332 else
333 return FSAEllipsisImage;
334 }
335 }
336 return nil;
337}
338
339@end
340
341@implementation FSAAppList (NSWindowDelegate)
342
343- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
344{
345 NSWindow *window = [tableView window];
346 NSRect frame = [window frame];
347 NSScrollView *scrollView = [tableView enclosingScrollView];
348 float displayedHeight = [[scrollView contentView] bounds].size.height;
349 float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
350 float heightExcess;
351
352 if (heightChange >= 0 && heightChange <= 1) {
353 // either the window is already optimal size, or it's too big
354 float rowHeight = [tableView cellHeight];
355 heightChange = (rowHeight * [tableView numberOfRows]) - displayedHeight;
356 }
357
358 frame.size.height += heightChange;
359
360 if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
361 (heightExcess = [window maxSize].height - frame.size.height) < 1) {
362 heightChange += heightExcess;
363 frame.size.height += heightExcess;
364 }
365
366 frame.origin.y -= heightChange;
367
368 return frame;
369}
370
371@end
372
373@implementation FSAAppList (NSApplicationDelegate)
374
375- (NSMenu *)applicationDockMenu:(NSApplication *)sender;
376{
377 static NSMenu *dockMenu = nil;
378 id<NSMenuItem> menuItem;
379 DeVercruesseProcess *frontApp = [processManager frontProcess];
380 NSString *appName = [frontApp name];
381 NSString *status = nil;
382 // XXX workaround for broken dock menu sender
383 NSMethodSignature *sig = [NSApp methodSignatureForSelector: @selector(installBundleInFrontmostApp:)];
384 NSInvocation *inv;
385
386 if (dockMenu != nil) {
387 // XXX release invocation
388 [[[dockMenu itemAtIndex: 0] target] release];
389 [dockMenu removeItemAtIndex: 0];
390 } else {
391 dockMenu = [[NSMenu alloc] init];
392 }
393
394 NSAssert(frontApp != nil && appName != nil, @"Can't obtain information on the frontmost application");
395
396 if ([patchedApps containsObject: frontApp]) {
397 status = [NSString stringWithFormat: NSLocalizedString(@"Installed in '%@'", "Dock menu disabled item displayed when FSA already installed, app name parameter"), appName];
398 } else if (![cocoaApps containsObject: frontApp]) {
399 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];
400 }
401
402 if (status == nil) {
403 menuItem = [dockMenu addItemWithTitle: [NSString stringWithFormat: NSLocalizedString(@"Install in '%@'", "Dock menu item to install FSA in frontmost app"), appName]
404 action: @selector(invoke)
405 keyEquivalent: @""];
406 inv = [NSInvocation invocationWithMethodSignature: sig];
407 [inv setSelector: @selector(installBundleInFrontmostApp:)];
408 [inv setTarget: NSApp];
409 [inv setArgument: &menuItem atIndex: 2];
410 [menuItem setTag: [frontApp pid]];
411 [menuItem setTarget: [inv retain]];
412 } else {
413 menuItem = [dockMenu addItemWithTitle: status action: nil keyEquivalent: @""];
414 [menuItem setEnabled: NO];
415 }
416
417 return dockMenu;
418}
419
420- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender;
421{
422 return YES;
423}
424
425@end
Note: See TracBrowser for help on using the repository browser.