source: trunk/Cocoa/F-Script Anywhere/Source/FSAApp.mm @ 230

Last change on this file since 230 was 230, checked in by rchin, 16 years ago

Auto installation of F-Script as requested by Philippe Mougin. We'll optimize the FSA framework package later (not sure if it would be a good idea to remove headers in framework, etc.)

File size: 19.8 KB
Line 
1//
2//  FSAApp.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 "FSAApp.h"
28#import "FSAnywhere.h"
29#import <DSCL/PathManager.h>
30#import <CoreFoundation/CoreFoundation.h>
31#import <ApplicationServices/ApplicationServices.h>
32#import <Security/AuthorizationTags.h>
33
34NSString * const PatchBundleIdentifier = @"net.sabi.FScriptAnywhere";
35NSString * const PatchBundleName = @"F-Script Anywhere.bundle";
36
37typedef struct {
38    OSStatus err;
39    NSString * const desc;
40} errRec, errList[];
41
42// These are just educated guesses based on the errors I've seen; I haven't seen these documented anywhere!
43static errList ERRS = { // XXX change for mach_inject/SCPatch
44    { 5, @"F-Script Anywhere must be installed in a Cocoa application running as the current user.\n\nYou may be attempting to install in a setuid application, which is not supported" },
45    { 11, @"F-Script Anywhere cannot be installed in itself.\n\nIf you wish to install F-Script Anywhere in itself, make a copy of the F-Script Anywhere application and install one copy into the other" },
46    { smUnExBusErr, @"a bus error occurred.\n\nTry switching to the application first then using F-Script AnywhereÕs dock menu to install" },
47    { fnfErr, @"F-Script Anywhere was unable to locate its component to install in the application.  Please try reinstalling F-Script Anywhere" },
48    { cfragDupRegistrationErr, @"another running copy of F-Script Anywhere is already installed in the application"},
49    { 0, nil }
50};
51
52NSMutableDictionary *FSA_errors;
53
54NSString * FSA_descriptionForOSStatus(OSStatus err)
55{
56    NSString *desc = nil;
57   
58    if (FSA_errors != nil)
59        desc = [FSA_errors objectForKey: [NSNumber numberWithLong: err]];
60   
61    if (desc != nil)
62        return desc;
63   
64    return [NSString stringWithFormat: NSLocalizedString(@"an error of type %ld occurred", "Unknown error message used after 'XXX happened because'"), err];
65}
66
67FSAPatchController::FSAPatchController(FSAApp *fsaApp)
68{
69    mApp = fsaApp;
70}
71
72void FSAPatchController::PatchNotification(ProcessSerialNumber *inPSN, OSType inCreator, OSType type, CFStringRef name, UInt32 flags) {
73    pid_t pid;
74    OSStatus err = GetProcessPID(inPSN, &pid);
75   
76    NSCAssert3(err == noErr, @"PatchNotification can't get PID for PSN %ld.%ld: error %ld", inPSN->highLongOfPSN, inPSN->lowLongOfPSN, err);
77   
78    FSALog(@"Got patch notification for %@ (pid %d)", (NSString *)name, pid);
79    [mApp controllerIsPatchingApplicationWithProcessID: pid];
80}
81
82// XXX should remove?
83void FSAPatchController::ReceiveMessage(const AppleEvent *theAE) {
84    OSErr err = noErr;
85    Size actualSize;
86    DescType actualType;
87    OSType eventClass, eventID;
88   
89    if ((err = AEGetAttributePtr(theAE, keyEventClassAttr, typeType, &actualType, &eventClass, sizeof(OSType), &actualSize)) != noErr)
90        return;
91    if (eventClass != kSCMessageClass)
92        return;
93
94    if ((err = AEGetAttributePtr(theAE, keyEventIDAttr, typeType, &actualType, &eventID, sizeof(OSType), &actualSize)) != noErr)
95        return;
96 
97#if 0
98    if (eventID == kSCLoadResult) {
99        OSStatus
100        (err = AEGetParamPtr(theAE, keyError, typeInteger, &actualType, &error, sizeof(long), &actualSize)) == noErr &&
101        (err = AEGetParamPtr(theAE, keyBundleID, typeText, &actualType, bundleID, sizeof(bundleID), &actualSize)) == noErr)
102        bundleID[actualSize] = 0;
103        printf("patch bundle %s loaded with error status %d\n", bundleID, error);
104    }
105#endif
106   
107
108}
109
110
111@implementation FSAApp
112
113+ (void)initialize;
114{
115    errRec *rec;
116
117    FSA_errors = [[NSMutableDictionary alloc] init];
118    for (rec = &(ERRS[0]) ; rec->err != 0 ; rec++)
119        [FSA_errors setObject: rec->desc forKey: [NSNumber numberWithLong: rec->err]];
120}
121
122-(void)installFScript
123{
124    OSStatus myStatus;
125
126    NSString *installPath = nil;
127    if([[NSFileManager defaultManager] fileExistsAtPath:@"/Library/Frameworks/FScript.framework"])
128        installPath = @"/Library/Frameworks/FScript.framework";
129    if([[NSFileManager defaultManager] fileExistsAtPath:[@"~/Library/Frameworks/FScript.framework" stringByExpandingTildeInPath]])
130        installPath = [@"~/Library/Frameworks/FScript.framework" stringByExpandingTildeInPath];
131   
132    if(installPath){
133        [[NSFileManager defaultManager] removeFileAtPath:installPath handler:nil];
134    }
135   
136    if(!installPath){
137        switch([[NSAlert alertWithMessageText:@"Install F-Script"
138                                defaultButton:@"Current User"
139                              alternateButton:@"All Users"
140                                  otherButton:nil
141                    informativeTextWithFormat:@"No previous version of F-Script found. You can choose to install for just your current user (~/Library/Frameworks) or for all users (/Library/Frameworks)."] runModal]){
142            case NSAlertDefaultReturn:  // Current user
143                installPath = [@"~/Library/Frameworks/FScript.framework" stringByExpandingTildeInPath];
144                break;
145            case NSAlertAlternateReturn: // All users
146                installPath = @"/Library/Frameworks/FScript.framework";
147                break;
148            default:
149                break;
150        }       
151    }
152   
153    NSString *fromPath = [[NSBundle mainBundle] pathForResource:@"FScript" ofType:@"framework"];
154    if(![[NSFileManager defaultManager] fileExistsAtPath:[installPath stringByDeletingLastPathComponent]]){
155        if ([[NSFileManager defaultManager] isWritableFileAtPath:installPath]){
156            [[NSFileManager defaultManager] createDirectoryAtPath:[installPath stringByDeletingLastPathComponent]
157                                                       attributes:nil];
158        } else {
159            [self createAuthorization];
160            NSString *myToolPath = @"/bin/mkdir";
161            char *myArguments[] = { (char *)[[installPath stringByDeletingLastPathComponent] UTF8String], NULL };
162            myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
163                                                          myFlags, myArguments,
164                                                          NULL);
165        }
166    }
167    if([[NSFileManager defaultManager] isWritableFileAtPath:[installPath stringByDeletingLastPathComponent]]){
168        [[NSFileManager defaultManager] copyPath:fromPath toPath:installPath handler:nil];
169    } else {
170        [self createAuthorization];
171        NSString *myToolPath = @"/bin/cp";
172        char *myArguments[] = { "-r", (char *)[fromPath UTF8String], (char *)[installPath UTF8String], NULL };
173        myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
174                                                      myFlags, myArguments,
175                                                      NULL);
176    }   
177}
178
179- (BOOL)_shouldContinueAfterFrameworkErrorPanelReturnedWithResult:(int)result;
180{
181    switch (result) {
182        case NSAlertOtherReturn:
183            return true;
184        case NSAlertAlternateReturn: /* Install from resources */
185            [self installFScript];
186            return true;
187        case NSAlertDefaultReturn:
188        default:
189            [self terminate: self];
190            return false;
191    }
192}
193
194-(BOOL)createAuthorization
195{
196    if(myAuthorizationRef)
197        return YES;
198   
199    OSStatus myStatus;
200    myFlags = kAuthorizationFlagDefaults;
201   
202    myStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
203                                   myFlags, &myAuthorizationRef);
204    if(myStatus != errAuthorizationSuccess)
205        return NO;
206   
207    AuthorizationItem myItems = {kAuthorizationRightExecute, 0, NULL, 0};
208    AuthorizationRights myRights = {1, &myItems};
209   
210    myFlags = kAuthorizationFlagDefaults
211        | kAuthorizationFlagInteractionAllowed
212        | kAuthorizationFlagPreAuthorize
213        | kAuthorizationFlagExtendRights;
214    myStatus = AuthorizationCopyRights(myAuthorizationRef, &myRights,
215                                       NULL, myFlags, NULL);
216   
217    if(myStatus != errAuthorizationSuccess)
218        return NO;
219   
220    myFlags = kAuthorizationFlagDefaults;
221    return YES;
222}
223
224-(tDirStatus)authorizeAndAddToProcMod:(NSString *)username
225{
226    OSStatus myStatus;
227   
228    if(![self createAuthorization])
229        return eDSAuthFailed;
230   
231    NSString *myToolPath = [[NSBundle mainBundle] pathForResource:@"AddToProcMod" ofType:@""];
232    char *myArguments[] = { (char *)[username UTF8String], NULL };
233    FILE *myCommunicationsPipe = NULL;
234    char myReadBuffer[128];
235    myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
236                                                  myFlags, myArguments,
237                                                  &myCommunicationsPipe);
238   
239    int didRead = 0;
240    int lastRead;
241    while((lastRead = read(fileno(myCommunicationsPipe), myReadBuffer, sizeof(myReadBuffer) - didRead - 1)) && (lastRead > 0))
242        didRead += lastRead;
243   
244    myReadBuffer[didRead - 1] = 0;
245   
246    return (tDirStatus)strtol(myReadBuffer, NULL, 10);
247}
248
249- (void)finishLaunching
250{
251#ifdef __i386__
252    if(![[NSUserDefaults standardUserDefaults] objectForKey:@"doPathCheck"])
253        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"doPathCheck"];
254   
255    if([[NSUserDefaults standardUserDefaults] boolForKey:@"doPathCheck"]){
256top:
257        PathManager *pm = [[PathManager alloc] initWithLocalNode];
258        [pm backupStack];
259        [pm cd:@"/Groups/procmod"];
260        CFDictionaryRef sessionInfoDict = CGSessionCopyCurrentDictionary();
261        if(sessionInfoDict){
262            CFStringRef shortUserName = (CFStringRef)CFDictionaryGetValue(sessionInfoDict, kCGSessionUserNameKey);
263            if(![[[pm lastObject] readArray:@"GroupMembership"] containsObject:(NSString *)shortUserName]){
264                switch([[NSAlert alertWithMessageText:[NSString stringWithFormat:@"User %@ not in the procmod group", shortUserName]
265                                        defaultButton:@"Add me"
266                                      alternateButton:@"Disable checking"
267                                          otherButton:@"Ignore message"
268                            informativeTextWithFormat:@"F-Script Anywhere requires that you add yourself to the procmod "
269                    "group in order for it to function properly. If you like, F-Script Anywhere can automatically add you "
270                    "to the procmod group."] runModal]){
271                    case NSAlertDefaultReturn:
272                    {
273                        tDirStatus status = [self authorizeAndAddToProcMod:(NSString *)shortUserName];
274                        if(status != eDSNoErr){
275                            [[NSAlert alertWithMessageText:@"Error adding to procmod group"
276                                             defaultButton:nil
277                                           alternateButton:nil
278                                               otherButton:nil
279                                 informativeTextWithFormat:@"There was an error (%@) adding you to the procmod group. ", [[NSClassFromString(@"DSoStatus") sharedInstance] stringForStatus:status]] runModal];
280                        }
281                    }
282                        [pm restoreStack];
283                        [pm release];
284                        goto top;
285                    case NSAlertAlternateReturn:
286                        [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"doPathCheck"];
287                        break;
288                    default:
289                        break;
290                }           
291            }
292        }
293        [pm restoreStack];
294        [pm release];
295    }
296#endif
297    patchController = new FSAPatchController(self);
298    patchController->AddPatch((CFStringRef)PatchBundleIdentifier, CFSTR("Contents/Resources/"),
299                              (CFStringRef)PatchBundleName);
300   
301    NSEnumerator *e = [[appList cocoaAppProcessIDs] objectEnumerator];
302    pid_t pid;
303    ProcessSerialNumber psn;
304    OSStatus err;
305   
306    while ( (pid = [[e nextObject] longValue]) != 0) {
307        err = GetProcessForPID(pid, &psn);
308       
309        if (err != noErr) {
310            NSLog(@"Can't get PSN for PID %ld", pid);
311            [appList applicationQuitWithProcessID: pid];
312        } else if (patchController->IsProcessPatched(&psn)) {
313            [appList didPatchProcessID: pid];
314        }
315    }
316
317    // yes, someone could move the framework while the app is running, but they deserve what they get.
318    NSFileManager *fileMgr = [NSFileManager defaultManager];
319    NSArray *libraryDirectories =
320        NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, TRUE);
321    NSString *libPath;
322    NSString *frameworkPath;
323    CFBundleRef frameworkBundle = NULL;
324    BOOL isDirectory;
325
326    e = [libraryDirectories objectEnumerator];
327    while ( (libPath = [e nextObject]) != nil) {
328        frameworkPath = [[libPath stringByAppendingPathComponent: @"Frameworks"]
329            stringByAppendingPathComponent: @"FScript.framework"];
330        if ([fileMgr fileExistsAtPath: frameworkPath isDirectory: &isDirectory] && isDirectory) {
331            frameworkBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)[NSURL fileURLWithPath: frameworkPath]);
332            [(id)frameworkBundle autorelease];
333            break;
334        }
335    }
336    FSALog(@"Framework bundle");
337    FSAShow(frameworkBundle);
338    [super finishLaunching];
339    if (frameworkBundle == NULL) {
340        int result = NSRunInformationalAlertPanel(
341            NSLocalizedString(@"F-Script Framework Not Found", "Framework not found alert title"),
342            NSLocalizedString(@"F-Script Anywhere requires the F-Script framework be installed in a Frameworks directory, such as ~/Library/Frameworks or /Library/Frameworks.\n\nTo download F-Script, please visit its Web site\n%@.\n\nThis package also includes a copy of F-Script for your convenience that can be installed easily by clicking below. Note that there may be a more recent version of F-Script available from the F-Script web site.\n\nIf you believe this message is in error, click '%@'.",
343                              "Framework not found alert message, F-Script Web site URL parameter, 'Continue' button parameter"),
344            NSLocalizedString(@"Quit", "Quit button title"),
345            NSLocalizedString(@"Install F-Script", "'Get F-Script' button title"),
346            NSLocalizedString(@"Continue", "Continue button title"),
347            FSA_FScriptURL, NSLocalizedString(@"Continue", ""));
348        if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result]) return;
349    } else {
350        UInt32 version = CFBundleGetVersionNumber(frameworkBundle);
351        FSALog(@"Framework version 0x%x", version);
352        if (version < FSA_FSCRIPT_MIN_VERSION) {
353            NSString *frameworkVersion = (NSString *)CFBundleGetValueForInfoDictionaryKey(frameworkBundle, kCFBundleVersionKey);
354            if (frameworkVersion == nil)
355                frameworkVersion = NSLocalizedString(@"unavailable", "Framework too old 'unavailable' version");
356            int result = NSRunInformationalAlertPanel(
357                NSLocalizedString(@"F-Script Framework Too Old", "Framework too old alert title"),
358                NSLocalizedString(@"F-Script Anywhere %s requires version %@ or later of the F-Script framework be installed in a Frameworks directory, such as ~/Library/Frameworks or /Library/Frameworks. The installed version is %@.\n\nTo upgrade F-Script, please visit its Web site\n%@.\n\nThis package also includes a copy of F-Script for your convenience that can be installed easily by clicking below. Note that there may be a more recent version of F-Script available from the F-Script web site.\n\nTo use this version of F-Script regardless of the problems you may experience, click '%@'.",
359                                  "Framework too old alert message, FSA version parameter, minimum F-Script version parameter, current F-Script version parameter, F-Script Web site URL parameter, 'Continue' button parameter"),
360                NSLocalizedString(@"Quit", "Quit button title"),
361                NSLocalizedString(@"Install F-Script", "'Get F-Script' button title"),
362                NSLocalizedString(@"Continue", "Continue button title"),
363                FSA_VERSION, FSA_FScriptMinimumVersion, frameworkVersion,
364                FSA_FScriptURL, NSLocalizedString(@"Install F-Script", ""));
365            if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result]) return;
366        }
367    }
368
369    if(myAuthorizationRef){
370        AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
371        myAuthorizationRef = nil;
372    }
373   
374    NSNotificationCenter *workspaceNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
375    [workspaceNotificationCenter
376        addObserver: self
377           selector: @selector(applicationDidLaunch:)
378               name: NSWorkspaceWillLaunchApplicationNotification
379             object: nil];
380    [workspaceNotificationCenter
381        addObserver: self
382           selector: @selector(applicationDidTerminate:)
383               name: NSWorkspaceDidTerminateApplicationNotification
384             object: nil];
385   
386    [appListWindow makeKeyAndOrderFront: self];
387}
388
389- (void)installationError:(OSStatus)err inAppWithPID:(pid_t)pid;
390{
391    NSBeginAlertSheet(NSLocalizedString(@"Installation failed", "Unable to install alert title"),
392                      nil, nil, nil, appListWindow, self, nil, nil, nil,
393                      NSLocalizedString(@"F-Script Anywhere was unable to install itself in the selected application (process ID %d), because %@.\n\nThe application may have crashed; restart it if needed.", "Unable to install alert message, PID parameter, 'XXX happened because' explanation parameter"),
394                      pid, FSA_descriptionForOSStatus(err));
395}
396
397- (void)installBundleInAppWithPID:(pid_t)pid;
398{
399    if (pid == -1) return;
400       
401    ProcessSerialNumber psn;
402    OSStatus err = GetProcessForPID(pid, &psn);
403   
404    if (err == noErr)
405        err = patchController->PatchProcess(&psn);
406
407    if (err != noErr)
408        [self installationError: err inAppWithPID: pid];
409    else
410        [appList didPatchProcessID: pid];
411}
412
413- (void)loaderBundleMessage:(NSNotification *)notification;
414{
415    /*
416    if (error) {
417        [self installationError: error inAppWithPID: pid];
418    } else {
419        [appList didPatchProcessID: pid];
420    } */
421}
422
423- (IBAction)installBundleInSelectedApp:(id)sender;
424{
425    [self installBundleInAppWithPID: [appList selectedProcessID]];
426}
427
428// from dock menu only!
429- (IBAction)installBundleInFrontmostApp:(id)sender;
430{
431    NSAssert1([sender tag] > 0, @"Unable to determine frontmost application from %@", sender);
432    [self installBundleInAppWithPID: (pid_t)[sender tag]];
433}
434
435@end
436
437@implementation FSAApp (FSAPatchControllerDelegate)
438
439- (void)controllerIsPatchingApplicationWithProcessID:(pid_t)pid;
440{
441    [appList isPatchingProcessID: pid];
442}
443
444@end
445
446@implementation FSAApp (NSWorkspaceNotifications)
447
448- (void)applicationDidLaunch:(NSNotification *)notification;
449{
450    [appList applicationLaunchedWithProcessID: [[[notification userInfo] objectForKey: @"NSApplicationProcessIdentifier"] intValue]];
451}
452
453- (void)applicationDidTerminate:(NSNotification *)notification;
454{
455    [appList applicationQuitWithProcessID: [[[notification userInfo] objectForKey: @"NSApplicationProcessIdentifier"] intValue]];
456}
457
458@end
Note: See TracBrowser for help on using the repository browser.