Prevent sending 0-byte files. Fixes #8711.
[adiumx.git] / Source / DiskImageUtilities.m
blob8ceddc81018ac5bb54284c0bc2d9d5a399652bc0
1 // sshfs.app
2 // Copyright 2007, Google Inc.
3 //
4 // Redistribution and use in source and binary forms, with or without 
5 // modification, are permitted provided that the following conditions are met:
6 //
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.
26 #include <unistd.h>
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;
40 @end
42 @implementation DiskImageUtilities
44 // get the mounted disk image info as a dictionary
45 + (NSDictionary *)diskImageInfo {
46   
47   NSDictionary *resultDict = nil;
48   
49   NSArray* args = [NSArray arrayWithObjects:@"info", @"-plist", nil];
50   // -plist means the results will be written as a property list to stdout
51   
52   NSPipe* outputPipe = [NSPipe pipe];
53   NSTask* theTask = [[[NSTask alloc] init] autorelease];
54   [theTask setLaunchPath:kHDIUtilPath];
55   [theTask setArguments:args];
56   [theTask setStandardOutput:outputPipe];
57   
58   [theTask launch];
59   
60   NSFileHandle *outputFile = [outputPipe fileHandleForReading];
61   NSData *plistData = nil;
62   @try {
63     plistData = [outputFile readDataToEndOfFile]; // blocks until EOF delivered
64   }
65   @catch(id obj) {
66     // running in gdb we get exception: Interrupted system call
67     NSLog(@"DiskImageUtilities diskImageInfo: gdb issue -- "
68                 "getting file data causes exception: %@", obj);
69   }
70   [theTask waitUntilExit];
71   int status = [theTask terminationStatus];
72   
73   if (status != 0 || [plistData length] == 0) {
74     
75     NSLog(@"DiskImageUtilities diskImageInfo: hdiutil failed, result %d", status); 
76     
77   } else {
78     NSString *plist = [[[NSString alloc] initWithData:plistData 
79                                              encoding:NSUTF8StringEncoding] autorelease];
80     resultDict = [plist propertyList];
81   }    
82   return resultDict;
85 + (NSArray *)readOnlyDiskImagePaths {
86   
87   NSMutableArray *paths = [NSMutableArray array];
88   NSDictionary *dict = [self diskImageInfo];
89   if (dict) {
90     
91     NSArray *imagesArray = [dict objectForKey:@"images"];
92     
93     // we have an array of dicts for the known images
94     //
95     // we want to find non-writable images, and get the mount
96     // points from their system entities
97     
98     if (imagesArray) {
99       int idx;
100       unsigned int numberOfImages = [imagesArray count];
101       for (idx = 0; idx < numberOfImages; idx++) {
102         
103         NSDictionary *imageDict = [imagesArray objectAtIndex:idx];
104         NSNumber *isWriteable = [imageDict objectForKey:@"writeable"];
105         if (isWriteable && ![isWriteable boolValue]) {
106           
107           NSArray *systemEntitiesArray = [imageDict objectForKey:@"system-entities"];
108           if (systemEntitiesArray) {
109             int idx;
110             unsigned int numberOfSystemEntities = [systemEntitiesArray count];
111             for (idx = 0; idx < numberOfSystemEntities; idx++) {
112               
113               NSDictionary *entityDict = [systemEntitiesArray objectAtIndex:idx];
114               NSString *mountPoint = [entityDict objectForKey:@"mount-point"];
115               if ([mountPoint length] > 0) {
116                 
117                 // found a read-only image mount point; add it to our list
118                 // and move to the next image
119                 [paths addObject:mountPoint];
120                 break;
121               }
122             }
123           }
124         }
125       }
126     }
127   }
128   return paths;
131 // checks if the current app is running from a disk image,
132 // displays a dialog offering to copy to /Applications, and
133 // does the copying
135 + (void)handleApplicationLaunchFromReadOnlyDiskImage {
136   
137   NSString * const kLastLaunchedPathKey = @"LastLaunchedPath";
138   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
139   
140   NSString *lastLaunchedPath = [defaults objectForKey:kLastLaunchedPathKey];
141   NSString *mainBundlePath = [[NSBundle mainBundle] bundlePath];
142   
143   BOOL isRunningFromDiskImage = NO;
144   
145   if (lastLaunchedPath == nil 
146       || ![lastLaunchedPath isEqualToString:mainBundlePath]) {
147     
148     // we haven't tested this launch path; check it now
149     NSArray *imagePaths = [self readOnlyDiskImagePaths];
150     
151     int idx;
152     for (idx = 0; idx < [imagePaths count]; idx++) {
153       
154       NSString *imagePath = [imagePaths objectAtIndex:idx];
155       if (![imagePath hasSuffix:@"/"])
156         imagePath = [NSString stringWithFormat:@"%@/", imagePath];
157       if ([mainBundlePath hasPrefix:imagePath]) {
158         
159         isRunningFromDiskImage = YES;
160         break;
161         
162       }
163     }
164     
165     // ? should we ask every time the user runs from a read-only disk image
166     if (!isRunningFromDiskImage) {
167       
168       // we don't need to check this bundle path again
169       [defaults setObject:mainBundlePath forKey:kLastLaunchedPathKey];
170       
171     } else {
172       // we're running from a disk image
173       
174       [NSApp activateIgnoringOtherApps:YES];
175       
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");
182       
183       int result = NSRunAlertPanel(msg1, msg2, btnOK, btnCancel, NULL, displayName);
184       if (result == NSAlertDefaultReturn) {
185         // copy to /Applications and launch from there
186         
187         NSArray *appsPaths = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
188                                                                  NSLocalDomainMask,
189                                                                  YES);
190         if ([appsPaths count] > 0) {
191           
192           [self copyAndLaunchAppAtPath:mainBundlePath
193                        toDirectoryPath:[appsPaths objectAtIndex:0]];  
194           // calls exit(0) on successful copy/launch
195         } else {
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,
199                                                         nil,
200                                                         nil,
201                                                         nil);
202                                                         
203                                                                         
204         }
205       }
206     }
207   }
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 {
214   
215   NSString *pathInApps = [newDirectory stringByAppendingPathComponent:[oldPath lastPathComponent]];
216   BOOL isDir;
217   BOOL dirPathExists = [[NSFileManager defaultManager]
218                       fileExistsAtPath:pathInApps isDirectory:&isDir] && isDir;
219   
220   AuthorizedTaskManager *authTaskMgr = [AuthorizedTaskManager sharedAuthorizedTaskManager];
221   
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]));
227   
228   if (!mustAuth || [authTaskMgr authorize]) {
229     
230     [self killAppAtPath:pathInApps];
231     
232     BOOL didCopy = [authTaskMgr copyPath:oldPath
233                                   toPath:pathInApps];
234     if (didCopy) {
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;
242       
243       OSStatus err = LSOpenFromURLSpec(&spec, NULL); // NULL -> don't care about the launched URL
244       if (err == noErr) {
245         exit(0);
246       } else {
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,
249                                                   nil,
250                                                   nil,
251                                                   nil);
252                   
253         NSLog(@"DiskImageUtilities: Error %d launching \"%@\"", err, pathInApps);
254       }
255     } else {
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,
260                                                 nil,
261                                                 nil,
262                                                 nil);
263                 
264     }
265   } else {
266     // user cancelled admin auth 
267   }
268   
271 // looks for an app running from the specified path, and calls KillProcess on it
272 + (void)killAppAtPath:(NSString *)appPath {
273   
274   // get the FSRef for bundle of the the target app to kill
275   FSRef targetFSRef;
276   OSStatus err = FSPathMakeRef((const UInt8 *)[appPath fileSystemRepresentation],
277                                &targetFSRef, nil);
278   if (err == noErr) {
279     
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) {
283       
284       FSRef compareFSRef;
285       if (GetProcessBundleLocation(&psn, &compareFSRef) == noErr
286           && FSCompareFSRefs(&compareFSRef, &targetFSRef) == noErr) {
287         
288         // we found an app running from that path; kill it
289         err = KillProcess(&psn);
290         if (err != noErr) {
291           NSLog(@"DiskImageUtilities: Could not kill process at %@, error %d",
292                 appPath, err);
293         } 
294       }
295     }
296   }
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));
302   return (stat == 0);
305 @end