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

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

Updated FScript framework zip to current working version (pre-release). Also added code to better do upgrading of existing outdated fscript framework versions. Also better checking for procmod existence.

File size: 23.2 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#include <mach/mach.h>
34
35NSString * const PatchBundleIdentifier = @"net.sabi.FScriptAnywhere";
36NSString * const PatchBundleName = @"F-Script Anywhere.bundle";
37
38typedef struct {
39    OSStatus err;
40    NSString * const desc;
41} errRec, errList[];
42
43// These are just educated guesses based on the errors I've seen; I haven't seen these documented anywhere!
44static errList ERRS = { // XXX change for mach_inject/SCPatch
45    { 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" },
46    { 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" },
47    { smUnExBusErr, @"a bus error occurred.\n\nTry switching to the application first then using F-Script AnywhereÕs dock menu to install" },
48    { fnfErr, @"F-Script Anywhere was unable to locate its component to install in the application.  Please try reinstalling F-Script Anywhere" },
49    { cfragDupRegistrationErr, @"another running copy of F-Script Anywhere is already installed in the application"},
50    { 0, nil }
51};
52
53NSMutableDictionary *FSA_errors;
54
55NSString * FSA_descriptionForOSStatus(OSStatus err)
56{
57    NSString *desc = nil;
58   
59    if (FSA_errors != nil)
60        desc = [FSA_errors objectForKey: [NSNumber numberWithLong: err]];
61   
62    if (desc != nil)
63        return desc;
64   
65    return [NSString stringWithFormat: NSLocalizedString(@"an error of type %ld occurred", "Unknown error message used after 'XXX happened because'"), err];
66}
67
68FSAPatchController::FSAPatchController(FSAApp *fsaApp)
69{
70    mApp = fsaApp;
71}
72
73void FSAPatchController::PatchNotification(ProcessSerialNumber *inPSN, OSType inCreator, OSType type, CFStringRef name, UInt32 flags) {
74    pid_t pid;
75    OSStatus err = GetProcessPID(inPSN, &pid);
76   
77    NSCAssert3(err == noErr, @"PatchNotification can't get PID for PSN %ld.%ld: error %ld", inPSN->highLongOfPSN, inPSN->lowLongOfPSN, err);
78   
79    FSALog(@"Got patch notification for %@ (pid %d)", (NSString *)name, pid);
80    [mApp controllerIsPatchingApplicationWithProcessID: pid];
81}
82
83// XXX should remove?
84void FSAPatchController::ReceiveMessage(const AppleEvent *theAE) {
85    OSErr err = noErr;
86    Size actualSize;
87    DescType actualType;
88    OSType eventClass, eventID;
89   
90    if ((err = AEGetAttributePtr(theAE, keyEventClassAttr, typeType, &actualType, &eventClass, sizeof(OSType), &actualSize)) != noErr)
91        return;
92    if (eventClass != kSCMessageClass)
93        return;
94
95    if ((err = AEGetAttributePtr(theAE, keyEventIDAttr, typeType, &actualType, &eventID, sizeof(OSType), &actualSize)) != noErr)
96        return;
97 
98#if 0
99    if (eventID == kSCLoadResult) {
100        OSStatus
101        (err = AEGetParamPtr(theAE, keyError, typeInteger, &actualType, &error, sizeof(long), &actualSize)) == noErr &&
102        (err = AEGetParamPtr(theAE, keyBundleID, typeText, &actualType, bundleID, sizeof(bundleID), &actualSize)) == noErr)
103        bundleID[actualSize] = 0;
104        printf("patch bundle %s loaded with error status %d\n", bundleID, error);
105    }
106#endif
107   
108
109}
110
111
112@implementation FSAApp
113
114+ (void)initialize;
115{
116    errRec *rec;
117
118    FSA_errors = [[NSMutableDictionary alloc] init];
119    for (rec = &(ERRS[0]) ; rec->err != 0 ; rec++)
120        [FSA_errors setObject: rec->desc forKey: [NSNumber numberWithLong: rec->err]];
121}
122
123-(void)installFScriptAtPath:(NSString *)installPath
124{
125    OSStatus myStatus;
126   
127    if(installPath){
128        if([installPath hasSuffix:@"FScript.framework"]){
129            installPath = [installPath stringByDeletingLastPathComponent];
130            NSLog(@"new ip %@", installPath);
131        }
132    }
133   
134    if(!installPath){
135        switch([[NSAlert alertWithMessageText:@"Install F-Script"
136                                defaultButton:@"Current User"
137                              alternateButton:@"All Users"
138                                  otherButton:nil
139                    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]){
140            case NSAlertDefaultReturn:  // Current user
141                installPath = [@"~/Library/Frameworks" stringByExpandingTildeInPath];
142                break;
143            case NSAlertAlternateReturn: // All users
144                installPath = @"/Library/Frameworks";
145                break;
146            default:
147                break;
148        }       
149    }
150   
151    NSString *fromPath = [[NSBundle mainBundle] pathForResource:@"FScript" ofType:@"framework"];
152    if(![[NSFileManager defaultManager] fileExistsAtPath:installPath]){
153        if ([[NSFileManager defaultManager] isWritableFileAtPath:installPath]){
154            [[NSFileManager defaultManager] createDirectoryAtPath:installPath
155                                                       attributes:nil];
156        } else {
157            [self createAuthorization];
158            NSString *myToolPath = @"/bin/mkdir";
159            char *myArguments[] = { (char *)[installPath UTF8String], NULL };
160            myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
161                                                          myFlags, myArguments,
162                                                          NULL);
163        }
164    }
165    if([[NSFileManager defaultManager] isWritableFileAtPath:installPath]){
166        [[NSFileManager defaultManager] copyPath:fromPath toPath:[installPath stringByAppendingString:@"/FScript.framework"] handler:nil];
167    } else {
168        [self createAuthorization];
169        NSString *myToolPath = @"/bin/cp";
170        char *myArguments[] = { "-R", "-f", (char *)[fromPath UTF8String], (char *)[installPath UTF8String], NULL };
171        myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
172                                                      myFlags, myArguments,
173                                                      NULL);
174    }   
175}
176
177- (BOOL)_shouldContinueAfterFrameworkErrorPanelReturnedWithResult:(int)result withFrameworkPath:(NSString *)frameworkPath
178{
179    switch (result) {
180        case NSAlertOtherReturn:
181            [self terminate: self];
182            return false;
183        case NSAlertAlternateReturn:
184            return true;
185        case NSAlertDefaultReturn:  /* Install from resources */
186            [self installFScriptAtPath:frameworkPath];
187            return true;
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    mach_port_t     taskOfOurProcess = mach_task_self();
252    mach_port_t     machPortForProcess;
253   
254    if(task_for_pid(taskOfOurProcess, 1, &machPortForProcess) != KERN_SUCCESS){ // launchd should always be pid 1
255        if(![[NSUserDefaults standardUserDefaults] objectForKey:@"doPathCheck"])
256            [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"doPathCheck"];
257       
258        if([[NSUserDefaults standardUserDefaults] boolForKey:@"doPathCheck"]){
259top:
260            PathManager *pm = [[PathManager alloc] initWithLocalNode];
261            [pm backupStack];
262            @try {
263                [pm cd:@"/Groups/procmod"];
264            } @catch ( NSException *exception ) {
265                goto next;  // this means we must be on ppc 10.4 or less.
266            }
267            CFDictionaryRef sessionInfoDict = CGSessionCopyCurrentDictionary();
268            if(sessionInfoDict){
269                CFStringRef shortUserName = (CFStringRef)CFDictionaryGetValue(sessionInfoDict, kCGSessionUserNameKey);
270                if(![[[pm lastObject] readArray:@"GroupMembership"] containsObject:(NSString *)shortUserName]){
271                    switch([[NSAlert alertWithMessageText:[NSString stringWithFormat:@"User %@ not in the procmod group", shortUserName]
272                                            defaultButton:@"Add me"
273                                          alternateButton:@"Disable checking"
274                                              otherButton:@"Ignore message"
275                                informativeTextWithFormat:@"F-Script Anywhere requires that you add yourself to the procmod "
276                        "group in order for it to function properly. If you like, F-Script Anywhere can automatically add you "
277                        "to the procmod group."] runModal]){
278                        case NSAlertDefaultReturn:
279                        {
280                            tDirStatus status = [self authorizeAndAddToProcMod:(NSString *)shortUserName];
281                            if(status != eDSNoErr){
282                                [[NSAlert alertWithMessageText:@"Error adding to procmod group"
283                                                 defaultButton:nil
284                                               alternateButton:nil
285                                                   otherButton:nil
286                                     informativeTextWithFormat:@"There was an error (%@) adding you to the procmod group. ", [[NSClassFromString(@"DSoStatus") sharedInstance] stringForStatus:status]] runModal];
287                            } else {
288                                [[NSAlert alertWithMessageText:@"Adding user procmod group succeeded"
289                                                 defaultButton:nil
290                                               alternateButton:nil
291                                                   otherButton:nil
292                                     informativeTextWithFormat:@"You may have to wait a few minutes until the system updates its caches (or alternatively, reboot your machine) before things will work properly."] runModal];
293                            }
294                        }
295                            [pm restoreStack];
296                            [pm release];
297                            goto top;
298                        case NSAlertAlternateReturn:
299                            [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"doPathCheck"];
300                            break;
301                        default:
302                            break;
303                    }           
304                }
305            }
306            [pm restoreStack];
307            [pm release];
308        }
309    } else {
310        mach_port_deallocate(taskOfOurProcess, machPortForProcess);
311    }
312next:
313    patchController = new FSAPatchController(self);
314    patchController->AddPatch((CFStringRef)PatchBundleIdentifier, CFSTR("Contents/Resources/"),
315                              (CFStringRef)PatchBundleName);
316   
317    NSEnumerator *e = [[appList cocoaAppProcessIDs] objectEnumerator];
318    pid_t pid;
319    ProcessSerialNumber psn;
320    OSStatus err;
321   
322    while ( (pid = [[e nextObject] longValue]) != 0) {
323        err = GetProcessForPID(pid, &psn);
324       
325        if (err != noErr) {
326            NSLog(@"Can't get PSN for PID %ld", pid);
327            [appList applicationQuitWithProcessID: pid];
328        } else if (patchController->IsProcessPatched(&psn)) {
329            [appList didPatchProcessID: pid];
330        }
331    }
332
333    // yes, someone could move the framework while the app is running, but they deserve what they get.
334    NSFileManager *fileMgr = [NSFileManager defaultManager];
335    NSArray *libraryDirectories =
336        NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, TRUE);
337    NSString *libPath;
338    NSString *frameworkPath = NULL;
339    CFBundleRef frameworkBundle = NULL;
340    BOOL isDirectory;
341
342    e = [libraryDirectories objectEnumerator];
343    while ( (libPath = [e nextObject]) != nil) {
344        frameworkPath = [[libPath stringByAppendingPathComponent: @"Frameworks"]
345            stringByAppendingPathComponent: @"FScript.framework"];
346        if ([fileMgr fileExistsAtPath: frameworkPath isDirectory: &isDirectory] && isDirectory) {
347            frameworkBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)[NSURL fileURLWithPath: frameworkPath]);
348            [(id)frameworkBundle autorelease];
349            break;
350        }
351    }
352    FSALog(@"Framework bundle");
353    FSAShow(frameworkBundle);
354    [super finishLaunching];
355    if (frameworkBundle == NULL) {
356        int result = NSRunInformationalAlertPanel(
357            NSLocalizedString(@"F-Script Framework Not Found", "Framework not found alert title"),
358            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 '%@'.",
359                              "Framework not found alert message, F-Script Web site URL parameter, 'Continue' button parameter"),
360            NSLocalizedString(@"Install F-Script", "'Get F-Script' button title"),
361            NSLocalizedString(@"Continue", "Continue button title"),
362            NSLocalizedString(@"Quit", "Quit button title"),
363            FSA_FScriptURL, NSLocalizedString(@"Continue", ""));
364        if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result withFrameworkPath:nil]) return;
365    } else {
366        CFBundleRef includedBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"FScript" ofType:@"framework"]]);
367        [(id)includedBundle autorelease];
368        UInt32 includedVersion = CFBundleGetVersionNumber(includedBundle);
369        UInt32 version = CFBundleGetVersionNumber(frameworkBundle);
370        FSALog(@"Framework version 0x%x", version);
371        if(version < includedVersion){
372            if (version < FSA_FSCRIPT_MIN_VERSION){ // must force upgrade
373                NSString *frameworkVersion = (NSString *)CFBundleGetValueForInfoDictionaryKey(frameworkBundle, kCFBundleVersionKey);
374                if (frameworkVersion == nil)
375                    frameworkVersion = NSLocalizedString(@"unavailable", "Framework too old 'unavailable' version");
376                int result = NSRunInformationalAlertPanel(
377                                                          NSLocalizedString(@"F-Script Framework Too Old", "Framework too old alert title"),
378                                                          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 to the latest version, 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 '%@'.",
379                                                                            "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"),
380                                                          NSLocalizedString(@"Install F-Script", "'Get F-Script' button title"),
381                                                          NSLocalizedString(@"Continue", "Continue button title"),
382                                                          NSLocalizedString(@"Quit", "Quit button title"),
383                                                          FSA_VERSION, FSA_FScriptMinimumVersion, frameworkVersion,
384                                                          FSA_FScriptURL, NSLocalizedString(@"Install F-Script", ""));
385                if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result withFrameworkPath:frameworkPath]) return;               
386            } else {
387                NSNumber *lastChecked = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastCheckedVersion"];
388                if((version < includedVersion) && (!lastChecked || ([lastChecked unsignedIntValue] != version))){
389                    FSALog(@"Included framework in resources is newer than installed version.");
390                    int result = NSRunInformationalAlertPanel(
391                                                              NSLocalizedString(@"Newer Version of F-Script Framework Available", "Framework newer found alert title"),
392                                                              NSLocalizedString(@"F-Script Anywhere is bundled with a newer version of the F-Script framework. If you wish, F-Script Anywhere can upgrade your framework version.",
393                                                                                "Framework newer found alert message, 'Continue' button parameter"),
394                                                              NSLocalizedString(@"Upgrade F-Script Framework", "Upgrade button title"),
395                                                              NSLocalizedString(@"Continue", "Continue button title"),
396                                                              nil,
397                                                              NSLocalizedString(@"Upgrade", ""), NSLocalizedString(@"Continue", ""));
398                    if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result withFrameworkPath:frameworkPath]) return;               
399                }
400            }
401        }
402        [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithUnsignedInt:version] forKey:@"LastCheckedVersion"];
403    }
404
405    if(myAuthorizationRef){
406        AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
407        myAuthorizationRef = nil;
408    }
409   
410    NSNotificationCenter *workspaceNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
411    [workspaceNotificationCenter
412        addObserver: self
413           selector: @selector(applicationDidLaunch:)
414               name: NSWorkspaceWillLaunchApplicationNotification
415             object: nil];
416    [workspaceNotificationCenter
417        addObserver: self
418           selector: @selector(applicationDidTerminate:)
419               name: NSWorkspaceDidTerminateApplicationNotification
420             object: nil];
421   
422    [appListWindow makeKeyAndOrderFront: self];
423}
424
425- (void)installationError:(OSStatus)err inAppWithPID:(pid_t)pid;
426{
427    NSBeginAlertSheet(NSLocalizedString(@"Installation failed", "Unable to install alert title"),
428                      nil, nil, nil, appListWindow, self, nil, nil, nil,
429                      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"),
430                      pid, FSA_descriptionForOSStatus(err));
431}
432
433- (void)installBundleInAppWithPID:(pid_t)pid;
434{
435    if (pid == -1) return;
436       
437    ProcessSerialNumber psn;
438    OSStatus err = GetProcessForPID(pid, &psn);
439   
440    if (err == noErr)
441        err = patchController->PatchProcess(&psn);
442
443    if (err != noErr)
444        [self installationError: err inAppWithPID: pid];
445    else
446        [appList didPatchProcessID: pid];
447}
448
449- (void)loaderBundleMessage:(NSNotification *)notification;
450{
451    /*
452    if (error) {
453        [self installationError: error inAppWithPID: pid];
454    } else {
455        [appList didPatchProcessID: pid];
456    } */
457}
458
459- (IBAction)installBundleInSelectedApp:(id)sender;
460{
461    [self installBundleInAppWithPID: [appList selectedProcessID]];
462}
463
464// from dock menu only!
465- (IBAction)installBundleInFrontmostApp:(id)sender;
466{
467    NSAssert1([sender tag] > 0, @"Unable to determine frontmost application from %@", sender);
468    [self installBundleInAppWithPID: (pid_t)[sender tag]];
469}
470
471@end
472
473@implementation FSAApp (FSAPatchControllerDelegate)
474
475- (void)controllerIsPatchingApplicationWithProcessID:(pid_t)pid;
476{
477    [appList isPatchingProcessID: pid];
478}
479
480@end
481
482@implementation FSAApp (NSWorkspaceNotifications)
483
484- (void)applicationDidLaunch:(NSNotification *)notification;
485{
486    [appList applicationLaunchedWithProcessID: [[[notification userInfo] objectForKey: @"NSApplicationProcessIdentifier"] intValue]];
487}
488
489- (void)applicationDidTerminate:(NSNotification *)notification;
490{
491    [appList applicationQuitWithProcessID: [[[notification userInfo] objectForKey: @"NSApplicationProcessIdentifier"] intValue]];
492}
493
494@end
Note: See TracBrowser for help on using the repository browser.