2 // Copyright 2007, Google Inc.
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
7 // 1. Redistributions of source code must retain the above copyright notice,
8 // this list of conditions and the following disclaimer.
9 // 2. Redistributions in binary form must reproduce the above copyright notice,
10 // this list of conditions and the following disclaimer in the documentation
11 // and/or other materials provided with the distribution.
12 // 3. The name of the author may not be used to endorse or promote products
13 // derived from this software without specific prior written permission.
15 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
16 // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
17 // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
18 // EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 #import "DiskImageUtilities.h"
29 #import "AuthorizedTaskManager.h"
31 #define INSTALL_ADIUM_MANUALLY_MESSAGE AILocalizedString(@"You will need to install Adium manually; please see Episode 0 at http://www.adiumx.com/screencasts/ for a video tutorial.", "Message shown if Adium can not install automatically. Don't localize the URL.")
33 static NSString* const kHDIUtilPath = @"/usr/bin/hdiutil";
35 @interface DiskImageUtilities (PrivateMethods_handleApplicationLaunchFromReadOnlyDiskImage)
36 + (BOOL)canWriteToPath:(NSString *)path;
37 + (void)copyAndLaunchAppAtPath:(NSString *)oldPath
38 toDirectoryPath:(NSString *)newDirectory;
39 + (void)killAppAtPath:(NSString *)appPath;
42 @implementation DiskImageUtilities
44 // get the mounted disk image info as a dictionary
45 + (NSDictionary *)diskImageInfo {
47 NSDictionary *resultDict = nil;
49 NSArray* args = [NSArray arrayWithObjects:@"info", @"-plist", nil];
50 // -plist means the results will be written as a property list to stdout
52 NSPipe* outputPipe = [NSPipe pipe];
53 NSTask* theTask = [[[NSTask alloc] init] autorelease];
54 [theTask setLaunchPath:kHDIUtilPath];
55 [theTask setArguments:args];
56 [theTask setStandardOutput:outputPipe];
60 NSFileHandle *outputFile = [outputPipe fileHandleForReading];
61 NSData *plistData = nil;
63 plistData = [outputFile readDataToEndOfFile]; // blocks until EOF delivered
66 // running in gdb we get exception: Interrupted system call
67 NSLog(@"DiskImageUtilities diskImageInfo: gdb issue -- "
68 "getting file data causes exception: %@", obj);
70 [theTask waitUntilExit];
71 int status = [theTask terminationStatus];
73 if (status != 0 || [plistData length] == 0) {
75 NSLog(@"DiskImageUtilities diskImageInfo: hdiutil failed, result %d", status);
78 NSString *plist = [[[NSString alloc] initWithData:plistData
79 encoding:NSUTF8StringEncoding] autorelease];
80 resultDict = [plist propertyList];
85 + (NSArray *)readOnlyDiskImagePaths {
87 NSMutableArray *paths = [NSMutableArray array];
88 NSDictionary *dict = [self diskImageInfo];
91 NSArray *imagesArray = [dict objectForKey:@"images"];
93 // we have an array of dicts for the known images
95 // we want to find non-writable images, and get the mount
96 // points from their system entities
100 unsigned int numberOfImages = [imagesArray count];
101 for (idx = 0; idx < numberOfImages; idx++) {
103 NSDictionary *imageDict = [imagesArray objectAtIndex:idx];
104 NSNumber *isWriteable = [imageDict objectForKey:@"writeable"];
105 if (isWriteable && ![isWriteable boolValue]) {
107 NSArray *systemEntitiesArray = [imageDict objectForKey:@"system-entities"];
108 if (systemEntitiesArray) {
110 unsigned int numberOfSystemEntities = [systemEntitiesArray count];
111 for (idx = 0; idx < numberOfSystemEntities; idx++) {
113 NSDictionary *entityDict = [systemEntitiesArray objectAtIndex:idx];
114 NSString *mountPoint = [entityDict objectForKey:@"mount-point"];
115 if ([mountPoint length] > 0) {
117 // found a read-only image mount point; add it to our list
118 // and move to the next image
119 [paths addObject:mountPoint];
131 // checks if the current app is running from a disk image,
132 // displays a dialog offering to copy to /Applications, and
135 + (void)handleApplicationLaunchFromReadOnlyDiskImage {
137 NSString * const kLastLaunchedPathKey = @"LastLaunchedPath";
138 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
140 NSString *lastLaunchedPath = [defaults objectForKey:kLastLaunchedPathKey];
141 NSString *mainBundlePath = [[NSBundle mainBundle] bundlePath];
143 BOOL isRunningFromDiskImage = NO;
145 if (lastLaunchedPath == nil
146 || ![lastLaunchedPath isEqualToString:mainBundlePath]) {
148 // we haven't tested this launch path; check it now
149 NSArray *imagePaths = [self readOnlyDiskImagePaths];
152 for (idx = 0; idx < [imagePaths count]; idx++) {
154 NSString *imagePath = [imagePaths objectAtIndex:idx];
155 if (![imagePath hasSuffix:@"/"])
156 imagePath = [NSString stringWithFormat:@"%@/", imagePath];
157 if ([mainBundlePath hasPrefix:imagePath]) {
159 isRunningFromDiskImage = YES;
165 // ? should we ask every time the user runs from a read-only disk image
166 if (!isRunningFromDiskImage) {
168 // we don't need to check this bundle path again
169 [defaults setObject:mainBundlePath forKey:kLastLaunchedPathKey];
172 // we're running from a disk image
174 [NSApp activateIgnoringOtherApps:YES];
176 NSString *displayName = [[NSFileManager defaultManager] displayNameAtPath:mainBundlePath];
177 NSString *msg1template = AILocalizedString(@"Would you like to copy %@ to your computer's Applications folder and run it from there?", "%@ will be replaced with 'Adium'");
178 NSString *msg1 = [NSString stringWithFormat:msg1template, displayName];
179 NSString *msg2 = AILocalizedString(@"%@ is currently running from the installation disk image. It needs to be copied for full functionality. Copying may replace an older version in the Applications folder.", "%@ will be replaced with 'Adium'.");
180 NSString *btnOK = AILocalizedString(@"Copy", "Button to copy Adium to the Applications folder from the disk image if needed");
181 NSString *btnCancel = AILocalizedString(@"Don't Copy", "Button to proceed without copying Adium to the Applications folder");
183 int result = NSRunAlertPanel(msg1, msg2, btnOK, btnCancel, NULL, displayName);
184 if (result == NSAlertDefaultReturn) {
185 // copy to /Applications and launch from there
187 NSArray *appsPaths = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
190 if ([appsPaths count] > 0) {
192 [self copyAndLaunchAppAtPath:mainBundlePath
193 toDirectoryPath:[appsPaths objectAtIndex:0]];
194 // calls exit(0) on successful copy/launch
196 NSLog(@"Cannot make applications folder path");
197 NSRunAlertPanel(AILocalizedString(@"Could not find your Applications folder", "Title of alert displayed if Adium can not find the Applications folder to perform a copy"),
198 INSTALL_ADIUM_MANUALLY_MESSAGE,
210 // copies an application from the given path to a new directory, if necessary
211 // authenticating as admin or killing a running process at that location
212 + (void)copyAndLaunchAppAtPath:(NSString *)oldPath
213 toDirectoryPath:(NSString *)newDirectory {
215 NSString *pathInApps = [newDirectory stringByAppendingPathComponent:[oldPath lastPathComponent]];
217 BOOL dirPathExists = [[NSFileManager defaultManager]
218 fileExistsAtPath:pathInApps isDirectory:&isDir] && isDir;
220 AuthorizedTaskManager *authTaskMgr = [AuthorizedTaskManager sharedAuthorizedTaskManager];
222 // We must authenticate as admin if we don't have write permission
223 // in the /Apps directory, or if there's already an app there
224 // with the same name and we don't have permission to write to it
225 BOOL mustAuth = (![self canWriteToPath:newDirectory]
226 || (dirPathExists && ![self canWriteToPath:pathInApps]));
228 if (!mustAuth || [authTaskMgr authorize]) {
230 [self killAppAtPath:pathInApps];
232 BOOL didCopy = [authTaskMgr copyPath:oldPath
235 // launch the new copy and bail
236 LSLaunchURLSpec spec;
237 spec.appURL = (CFURLRef) [NSURL fileURLWithPath:pathInApps];
238 spec.launchFlags = kLSLaunchNewInstance;
239 spec.itemURLs = NULL;
240 spec.passThruParams = NULL;
241 spec.asyncRefCon = NULL;
243 OSStatus err = LSOpenFromURLSpec(&spec, NULL); // NULL -> don't care about the launched URL
247 NSRunAlertPanel(AILocalizedString(@"Could not open Adium after installation", "Title of alert displayed if Adium can not launch after attempting an installation"),
248 INSTALL_ADIUM_MANUALLY_MESSAGE,
253 NSLog(@"DiskImageUtilities: Error %d launching \"%@\"", err, pathInApps);
256 // copying to /Applications failed
257 NSLog(@"DiskImageUtilities: Error copying to \"%@\"", pathInApps);
258 NSRunAlertPanel(AILocalizedString(@"Could not copy to your Applications folder", "Title of alert displayed if Adium can not copy while attempting an installation"),
259 INSTALL_ADIUM_MANUALLY_MESSAGE,
266 // user cancelled admin auth
271 // looks for an app running from the specified path, and calls KillProcess on it
272 + (void)killAppAtPath:(NSString *)appPath {
274 // get the FSRef for bundle of the the target app to kill
276 OSStatus err = FSPathMakeRef((const UInt8 *)[appPath fileSystemRepresentation],
280 // search for a PSN of a process with the bundle at that location, if any
281 ProcessSerialNumber psn = { 0, kNoProcess };
282 while (GetNextProcess(&psn) == noErr) {
285 if (GetProcessBundleLocation(&psn, &compareFSRef) == noErr
286 && FSCompareFSRefs(&compareFSRef, &targetFSRef) == noErr) {
288 // we found an app running from that path; kill it
289 err = KillProcess(&psn);
291 NSLog(@"DiskImageUtilities: Could not kill process at %@, error %d",
299 // canWriteToPath checks for permissions to write into the directory |path|
300 + (BOOL)canWriteToPath:(NSString *)path {
301 int stat = access([path fileSystemRepresentation], (W_OK | R_OK));