Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / mac / install_from_dmg.mm
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/mac/install_from_dmg.h"
6
7 #import <AppKit/AppKit.h>
8 #include <ApplicationServices/ApplicationServices.h>
9 #include <CoreFoundation/CoreFoundation.h>
10 #include <CoreServices/CoreServices.h>
11 #include <DiskArbitration/DiskArbitration.h>
12 #include <IOKit/IOKitLib.h>
13 #include <signal.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <sys/mount.h>
17 #include <sys/param.h>
18
19 #include "base/auto_reset.h"
20 #include "base/basictypes.h"
21 #include "base/command_line.h"
22 #include "base/files/file_path.h"
23 #include "base/logging.h"
24 #include "base/mac/authorization_util.h"
25 #include "base/mac/bundle_locations.h"
26 #include "base/mac/mac_logging.h"
27 #import "base/mac/mac_util.h"
28 #include "base/mac/mach_logging.h"
29 #include "base/mac/scoped_authorizationref.h"
30 #include "base/mac/scoped_cftyperef.h"
31 #include "base/mac/scoped_ioobject.h"
32 #include "base/mac/scoped_nsautorelease_pool.h"
33 #include "base/strings/string_util.h"
34 #include "base/strings/sys_string_conversions.h"
35 #include "chrome/browser/mac/dock.h"
36 #import "chrome/browser/mac/keystone_glue.h"
37 #include "chrome/browser/mac/relauncher.h"
38 #include "chrome/common/chrome_constants.h"
39 #include "chrome/grit/chromium_strings.h"
40 #include "chrome/grit/generated_resources.h"
41 #include "ui/base/l10n/l10n_util.h"
42 #include "ui/base/l10n/l10n_util_mac.h"
43
44 // When C++ exceptions are disabled, the C++ library defines |try| and
45 // |catch| so as to allow exception-expecting C++ code to build properly when
46 // language support for exceptions is not present.  These macros interfere
47 // with the use of |@try| and |@catch| in Objective-C files such as this one.
48 // Undefine these macros here, after everything has been #included, since
49 // there will be no C++ uses and only Objective-C uses from this point on.
50 #undef try
51 #undef catch
52
53 namespace {
54
55 // Given an io_service_t (expected to be of class IOMedia), walks the ancestor
56 // chain, returning the closest ancestor that implements class IOHDIXHDDrive,
57 // if any. If no such ancestor is found, returns NULL. Following the "copy"
58 // rule, the caller assumes ownership of the returned value.
59 //
60 // Note that this looks for a class that inherits from IOHDIXHDDrive, but it
61 // will not likely find a concrete IOHDIXHDDrive. It will be
62 // IOHDIXHDDriveOutKernel for disk images mounted "out-of-kernel" or
63 // IOHDIXHDDriveInKernel for disk images mounted "in-kernel." Out-of-kernel is
64 // the default as of Mac OS X 10.5. See the documentation for "hdiutil attach
65 // -kernel" for more information.
66 io_service_t CopyHDIXDriveServiceForMedia(io_service_t media) {
67   const char disk_image_class[] = "IOHDIXHDDrive";
68
69   // This is highly unlikely. media as passed in is expected to be of class
70   // IOMedia. Since the media service's entire ancestor chain will be checked,
71   // though, check it as well.
72   if (IOObjectConformsTo(media, disk_image_class)) {
73     IOObjectRetain(media);
74     return media;
75   }
76
77   io_iterator_t iterator_ref;
78   kern_return_t kr =
79       IORegistryEntryCreateIterator(media,
80                                     kIOServicePlane,
81                                     kIORegistryIterateRecursively |
82                                         kIORegistryIterateParents,
83                                     &iterator_ref);
84   if (kr != KERN_SUCCESS) {
85     MACH_LOG(ERROR, kr) << "IORegistryEntryCreateIterator";
86     return IO_OBJECT_NULL;
87   }
88   base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
89   iterator_ref = IO_OBJECT_NULL;
90
91   // Look at each of the ancestor services, beginning with the parent,
92   // iterating all the way up to the device tree's root. If any ancestor
93   // service matches the class used for disk images, the media resides on a
94   // disk image, and the disk image file's path can be determined by examining
95   // the image-path property.
96   for (base::mac::ScopedIOObject<io_service_t> ancestor(
97            IOIteratorNext(iterator));
98        ancestor;
99        ancestor.reset(IOIteratorNext(iterator))) {
100     if (IOObjectConformsTo(ancestor, disk_image_class)) {
101       return ancestor.release();
102     }
103   }
104
105   // The media does not reside on a disk image.
106   return IO_OBJECT_NULL;
107 }
108
109 // Given an io_service_t (expected to be of class IOMedia), determines whether
110 // that service is on a disk image. If it is, returns true. If image_path is
111 // present, it will be set to the pathname of the disk image file, encoded in
112 // filesystem encoding.
113 bool MediaResidesOnDiskImage(io_service_t media, std::string* image_path) {
114   if (image_path) {
115     image_path->clear();
116   }
117
118   base::mac::ScopedIOObject<io_service_t> hdix_drive(
119       CopyHDIXDriveServiceForMedia(media));
120   if (!hdix_drive) {
121     return false;
122   }
123
124   if (image_path) {
125     base::ScopedCFTypeRef<CFTypeRef> image_path_cftyperef(
126         IORegistryEntryCreateCFProperty(
127             hdix_drive, CFSTR("image-path"), NULL, 0));
128     if (!image_path_cftyperef) {
129       LOG(ERROR) << "IORegistryEntryCreateCFProperty";
130       return true;
131     }
132     if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) {
133       base::ScopedCFTypeRef<CFStringRef> observed_type_cf(
134           CFCopyTypeIDDescription(CFGetTypeID(image_path_cftyperef)));
135       std::string observed_type;
136       if (observed_type_cf) {
137         observed_type.assign(", observed ");
138         observed_type.append(base::SysCFStringRefToUTF8(observed_type_cf));
139       }
140       LOG(ERROR) << "image-path: expected CFData, observed " << observed_type;
141       return true;
142     }
143
144     CFDataRef image_path_data = static_cast<CFDataRef>(
145         image_path_cftyperef.get());
146     CFIndex length = CFDataGetLength(image_path_data);
147     if (length <= 0) {
148       LOG(ERROR) << "image_path_data is unexpectedly empty";
149       return true;
150     }
151     char* image_path_c = WriteInto(image_path, length + 1);
152     CFDataGetBytes(image_path_data,
153                    CFRangeMake(0, length),
154                    reinterpret_cast<UInt8*>(image_path_c));
155   }
156
157   return true;
158 }
159
160 // Returns true if |path| is located on a read-only filesystem of a disk
161 // image. Returns false if not, or in the event of an error. If
162 // out_dmg_bsd_device_name is present, it will be set to the BSD device name
163 // for the disk image's device, in "diskNsM" form.
164 bool IsPathOnReadOnlyDiskImage(const char path[],
165                                std::string* out_dmg_bsd_device_name) {
166   if (out_dmg_bsd_device_name) {
167     out_dmg_bsd_device_name->clear();
168   }
169
170   struct statfs statfs_buf;
171   if (statfs(path, &statfs_buf) != 0) {
172     PLOG(ERROR) << "statfs " << path;
173     return false;
174   }
175
176   if (!(statfs_buf.f_flags & MNT_RDONLY)) {
177     // Not on a read-only filesystem.
178     return false;
179   }
180
181   const char dev_root[] = "/dev/";
182   const int dev_root_length = arraysize(dev_root) - 1;
183   if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
184     // Not rooted at dev_root, no BSD name to search on.
185     return false;
186   }
187
188   // BSD names in IOKit don't include dev_root.
189   const char* dmg_bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
190   if (out_dmg_bsd_device_name) {
191     out_dmg_bsd_device_name->assign(dmg_bsd_device_name);
192   }
193
194   const mach_port_t master_port = kIOMasterPortDefault;
195
196   // IOBSDNameMatching gives ownership of match_dict to the caller, but
197   // IOServiceGetMatchingServices will assume that reference.
198   CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
199                                                         0,
200                                                         dmg_bsd_device_name);
201   if (!match_dict) {
202     LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name;
203     return false;
204   }
205
206   io_iterator_t iterator_ref;
207   kern_return_t kr = IOServiceGetMatchingServices(master_port,
208                                                   match_dict,
209                                                   &iterator_ref);
210   if (kr != KERN_SUCCESS) {
211     MACH_LOG(ERROR, kr) << "IOServiceGetMatchingServices";
212     return false;
213   }
214   base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref);
215   iterator_ref = IO_OBJECT_NULL;
216
217   // There needs to be exactly one matching service.
218   base::mac::ScopedIOObject<io_service_t> media(IOIteratorNext(iterator));
219   if (!media) {
220     LOG(ERROR) << "IOIteratorNext: no service";
221     return false;
222   }
223   base::mac::ScopedIOObject<io_service_t> unexpected_service(
224       IOIteratorNext(iterator));
225   if (unexpected_service) {
226     LOG(ERROR) << "IOIteratorNext: too many services";
227     return false;
228   }
229
230   iterator.reset();
231
232   return MediaResidesOnDiskImage(media, NULL);
233 }
234
235 // Returns true if the application is located on a read-only filesystem of a
236 // disk image. Returns false if not, or in the event of an error. If
237 // dmg_bsd_device_name is present, it will be set to the BSD device name for
238 // the disk image's device, in "diskNsM" form.
239 bool IsAppRunningFromReadOnlyDiskImage(std::string* dmg_bsd_device_name) {
240   return IsPathOnReadOnlyDiskImage(
241       [[base::mac::OuterBundle() bundlePath] fileSystemRepresentation],
242       dmg_bsd_device_name);
243 }
244
245 // Shows a dialog asking the user whether or not to install from the disk
246 // image.  Returns true if the user approves installation.
247 bool ShouldInstallDialog() {
248   NSString* title = l10n_util::GetNSStringFWithFixup(
249       IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
250   NSString* prompt = l10n_util::GetNSStringFWithFixup(
251       IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
252   NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
253   NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
254
255   NSAlert* alert = [[[NSAlert alloc] init] autorelease];
256
257   [alert setAlertStyle:NSInformationalAlertStyle];
258   [alert setMessageText:title];
259   [alert setInformativeText:prompt];
260   [alert addButtonWithTitle:yes];
261   NSButton* cancel_button = [alert addButtonWithTitle:no];
262   [cancel_button setKeyEquivalent:@"\e"];
263
264   NSInteger result = [alert runModal];
265
266   return result == NSAlertFirstButtonReturn;
267 }
268
269 // Potentially shows an authorization dialog to request authentication to
270 // copy.  If application_directory appears to be unwritable, attempts to
271 // obtain authorization, which may result in the display of the dialog.
272 // Returns NULL if authorization is not performed because it does not appear
273 // to be necessary because the user has permission to write to
274 // application_directory.  Returns NULL if authorization fails.
275 AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
276   NSFileManager* file_manager = [NSFileManager defaultManager];
277   if ([file_manager isWritableFileAtPath:application_directory]) {
278     return NULL;
279   }
280
281   NSString* prompt = l10n_util::GetNSStringFWithFixup(
282       IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
283       l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
284   return base::mac::AuthorizationCreateToRunAsRoot(
285       base::mac::NSToCFCast(prompt));
286 }
287
288 // Invokes the installer program at installer_path to copy source_path to
289 // target_path and perform any additional on-disk bookkeeping needed to be
290 // able to launch target_path properly.  If authorization_arg is non-NULL,
291 // function will assume ownership of it, will invoke the installer with that
292 // authorization reference, and will attempt Keystone ticket promotion.
293 bool InstallFromDiskImage(AuthorizationRef authorization_arg,
294                           NSString* installer_path,
295                           NSString* source_path,
296                           NSString* target_path) {
297   base::mac::ScopedAuthorizationRef authorization(authorization_arg);
298   authorization_arg = NULL;
299   int exit_status;
300   if (authorization) {
301     const char* installer_path_c = [installer_path fileSystemRepresentation];
302     const char* source_path_c = [source_path fileSystemRepresentation];
303     const char* target_path_c = [target_path fileSystemRepresentation];
304     const char* arguments[] = {source_path_c, target_path_c, NULL};
305
306     OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
307         authorization,
308         installer_path_c,
309         kAuthorizationFlagDefaults,
310         arguments,
311         NULL,  // pipe
312         &exit_status);
313     if (status != errAuthorizationSuccess) {
314       OSSTATUS_LOG(ERROR, status)
315           << "AuthorizationExecuteWithPrivileges install";
316       return false;
317     }
318   } else {
319     NSArray* arguments = [NSArray arrayWithObjects:source_path,
320                                                    target_path,
321                                                    nil];
322
323     NSTask* task;
324     @try {
325       task = [NSTask launchedTaskWithLaunchPath:installer_path
326                                       arguments:arguments];
327     } @catch(NSException* exception) {
328       LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
329                  << [[exception description] UTF8String];
330       return false;
331     }
332
333     [task waitUntilExit];
334     exit_status = [task terminationStatus];
335   }
336
337   if (exit_status != 0) {
338     LOG(ERROR) << "install.sh: exit status " << exit_status;
339     return false;
340   }
341
342   if (authorization) {
343     // As long as an AuthorizationRef is available, promote the Keystone
344     // ticket.  Inform KeystoneGlue of the new path to use.
345     KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
346     [keystone_glue setAppPath:target_path];
347     [keystone_glue promoteTicketWithAuthorization:authorization.release()
348                                       synchronous:YES];
349   }
350
351   return true;
352 }
353
354 // Launches the application at installed_path. The helper application
355 // contained within install_path will be used for the relauncher process. This
356 // keeps Launch Services from ever having to see or think about the helper
357 // application on the disk image. The relauncher process will be asked to
358 // call EjectAndTrashDiskImage on dmg_bsd_device_name.
359 bool LaunchInstalledApp(NSString* installed_path,
360                         const std::string& dmg_bsd_device_name) {
361   base::FilePath browser_path([installed_path fileSystemRepresentation]);
362
363   base::FilePath helper_path = browser_path.Append("Contents/Versions");
364   helper_path = helper_path.Append(chrome::kChromeVersion);
365   helper_path = helper_path.Append(chrome::kHelperProcessExecutablePath);
366
367   std::vector<std::string> args =
368       CommandLine::ForCurrentProcess()->argv();
369   args[0] = browser_path.value();
370
371   std::vector<std::string> relauncher_args;
372   if (!dmg_bsd_device_name.empty()) {
373     std::string dmg_arg(mac_relauncher::kRelauncherDMGDeviceArg);
374     dmg_arg.append(dmg_bsd_device_name);
375     relauncher_args.push_back(dmg_arg);
376   }
377
378   return mac_relauncher::RelaunchAppWithHelper(helper_path.value(),
379                                                relauncher_args,
380                                                args);
381 }
382
383 void ShowErrorDialog() {
384   NSString* title = l10n_util::GetNSStringWithFixup(
385       IDS_INSTALL_FROM_DMG_ERROR_TITLE);
386   NSString* error = l10n_util::GetNSStringFWithFixup(
387       IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
388   NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
389
390   NSAlert* alert = [[[NSAlert alloc] init] autorelease];
391
392   [alert setAlertStyle:NSWarningAlertStyle];
393   [alert setMessageText:title];
394   [alert setInformativeText:error];
395   [alert addButtonWithTitle:ok];
396
397   [alert runModal];
398 }
399
400 }  // namespace
401
402 bool MaybeInstallFromDiskImage() {
403   base::mac::ScopedNSAutoreleasePool autorelease_pool;
404
405   std::string dmg_bsd_device_name;
406   if (!IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name)) {
407     return false;
408   }
409
410   NSArray* application_directories =
411       NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
412                                           NSLocalDomainMask,
413                                           YES);
414   if ([application_directories count] == 0) {
415     LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
416                << "no local application directories";
417     return false;
418   }
419   NSString* application_directory = [application_directories objectAtIndex:0];
420
421   NSFileManager* file_manager = [NSFileManager defaultManager];
422
423   BOOL is_directory;
424   if (![file_manager fileExistsAtPath:application_directory
425                           isDirectory:&is_directory] ||
426       !is_directory) {
427     VLOG(1) << "No application directory at "
428             << [application_directory UTF8String];
429     return false;
430   }
431
432   NSString* source_path = [base::mac::OuterBundle() bundlePath];
433   NSString* application_name = [source_path lastPathComponent];
434   NSString* target_path =
435       [application_directory stringByAppendingPathComponent:application_name];
436
437   if ([file_manager fileExistsAtPath:target_path]) {
438     VLOG(1) << "Something already exists at " << [target_path UTF8String];
439     return false;
440   }
441
442   NSString* installer_path =
443       [base::mac::FrameworkBundle() pathForResource:@"install" ofType:@"sh"];
444   if (!installer_path) {
445     VLOG(1) << "Could not locate install.sh";
446     return false;
447   }
448
449   if (!ShouldInstallDialog()) {
450     return false;
451   }
452
453   base::mac::ScopedAuthorizationRef authorization(
454       MaybeShowAuthorizationDialog(application_directory));
455   // authorization will be NULL if it's deemed unnecessary or if
456   // authentication fails.  In either case, try to install without privilege
457   // escalation.
458
459   if (!InstallFromDiskImage(authorization.release(),
460                             installer_path,
461                             source_path,
462                             target_path)) {
463     ShowErrorDialog();
464     return false;
465   }
466
467   dock::AddIcon(target_path, source_path);
468
469   if (dmg_bsd_device_name.empty()) {
470     // Not fatal, just diagnostic.
471     LOG(ERROR) << "Could not determine disk image BSD device name";
472   }
473
474   if (!LaunchInstalledApp(target_path, dmg_bsd_device_name)) {
475     ShowErrorDialog();
476     return false;
477   }
478
479   return true;
480 }
481
482 namespace {
483
484 // A simple scoper that calls DASessionScheduleWithRunLoop when created and
485 // DASessionUnscheduleFromRunLoop when destroyed.
486 class ScopedDASessionScheduleWithRunLoop {
487  public:
488   ScopedDASessionScheduleWithRunLoop(DASessionRef session,
489                                      CFRunLoopRef run_loop,
490                                      CFStringRef run_loop_mode)
491       : session_(session),
492         run_loop_(run_loop),
493         run_loop_mode_(run_loop_mode) {
494     DASessionScheduleWithRunLoop(session_, run_loop_, run_loop_mode_);
495   }
496
497   ~ScopedDASessionScheduleWithRunLoop() {
498     DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_);
499   }
500
501  private:
502   DASessionRef session_;
503   CFRunLoopRef run_loop_;
504   CFStringRef run_loop_mode_;
505
506   DISALLOW_COPY_AND_ASSIGN(ScopedDASessionScheduleWithRunLoop);
507 };
508
509 // A small structure used to ferry data between SynchronousDAOperation and
510 // SynchronousDACallbackAdapter.
511 struct SynchronousDACallbackData {
512  public:
513   SynchronousDACallbackData()
514       : callback_called(false),
515         run_loop_running(false) {
516   }
517
518   base::ScopedCFTypeRef<DADissenterRef> dissenter;
519   bool callback_called;
520   bool run_loop_running;
521
522  private:
523   DISALLOW_COPY_AND_ASSIGN(SynchronousDACallbackData);
524 };
525
526 // The callback target for SynchronousDAOperation. Set the fields in
527 // SynchronousDACallbackData properly and then stops the run loop so that
528 // SynchronousDAOperation may proceed.
529 void SynchronousDACallbackAdapter(DADiskRef disk,
530                                   DADissenterRef dissenter,
531                                   void* context) {
532   SynchronousDACallbackData* callback_data =
533       static_cast<SynchronousDACallbackData*>(context);
534   callback_data->callback_called = true;
535
536   if (dissenter) {
537     CFRetain(dissenter);
538     callback_data->dissenter.reset(dissenter);
539   }
540
541   // Only stop the run loop if SynchronousDAOperation started it. Don't stop
542   // anything if this callback was reached synchronously from DADiskUnmount or
543   // DADiskEject.
544   if (callback_data->run_loop_running) {
545     CFRunLoopStop(CFRunLoopGetCurrent());
546   }
547 }
548
549 // Performs a DiskArbitration operation synchronously. After the operation is
550 // requested by SynchronousDADiskUnmount or SynchronousDADiskEject, those
551 // functions will call this one to run a run loop for a period of time,
552 // waiting for the callback to be called. When the callback is called, the
553 // run loop will be stopped, and this function will examine the result. If
554 // a dissenter prevented the operation from completing, or if the run loop
555 // timed out without the callback being called, this function will return
556 // false. When the callback completes successfully with no dissenters within
557 // the time allotted, this function returns true. This function requires that
558 // the DASession being used for the operation being performed has been added
559 // to the current run loop with DASessionScheduleWithRunLoop.
560 bool SynchronousDAOperation(const char* name,
561                             SynchronousDACallbackData* callback_data) {
562   // The callback may already have been called synchronously. In that case,
563   // avoid spinning the run loop at all.
564   if (!callback_data->callback_called) {
565     const CFTimeInterval kOperationTimeoutSeconds = 15;
566     base::AutoReset<bool> running_reset(&callback_data->run_loop_running, true);
567     CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE);
568   }
569
570   if (!callback_data->callback_called) {
571     LOG(ERROR) << name << ": timed out";
572     return false;
573   } else if (callback_data->dissenter) {
574     CFStringRef status_string_cf =
575         DADissenterGetStatusString(callback_data->dissenter);
576     std::string status_string;
577     if (status_string_cf) {
578       status_string.assign(" ");
579       status_string.append(base::SysCFStringRefToUTF8(status_string_cf));
580     }
581     LOG(ERROR) << name << ": dissenter: "
582                << DADissenterGetStatus(callback_data->dissenter)
583                << status_string;
584     return false;
585   }
586
587   return true;
588 }
589
590 // Calls DADiskUnmount synchronously, returning the result.
591 bool SynchronousDADiskUnmount(DADiskRef disk, DADiskUnmountOptions options) {
592   SynchronousDACallbackData callback_data;
593   DADiskUnmount(disk, options, SynchronousDACallbackAdapter, &callback_data);
594   return SynchronousDAOperation("DADiskUnmount", &callback_data);
595 }
596
597 // Calls DADiskEject synchronously, returning the result.
598 bool SynchronousDADiskEject(DADiskRef disk, DADiskEjectOptions options) {
599   SynchronousDACallbackData callback_data;
600   DADiskEject(disk, options, SynchronousDACallbackAdapter, &callback_data);
601   return SynchronousDAOperation("DADiskEject", &callback_data);
602 }
603
604 }  // namespace
605
606 void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) {
607   base::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL));
608   if (!session.get()) {
609     LOG(ERROR) << "DASessionCreate";
610     return;
611   }
612
613   base::ScopedCFTypeRef<DADiskRef> disk(
614       DADiskCreateFromBSDName(NULL, session, dmg_bsd_device_name.c_str()));
615   if (!disk.get()) {
616     LOG(ERROR) << "DADiskCreateFromBSDName";
617     return;
618   }
619
620   // dmg_bsd_device_name may only refer to part of the disk: it may be a
621   // single filesystem on a larger disk. Use the "whole disk" object to
622   // be able to unmount all mounted filesystems from the disk image, and eject
623   // the image. This is harmless if dmg_bsd_device_name already referred to a
624   // "whole disk."
625   disk.reset(DADiskCopyWholeDisk(disk));
626   if (!disk.get()) {
627     LOG(ERROR) << "DADiskCopyWholeDisk";
628     return;
629   }
630
631   base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk));
632   if (!media.get()) {
633     LOG(ERROR) << "DADiskCopyIOMedia";
634     return;
635   }
636
637   // Make sure the device is a disk image, and get the path to its disk image
638   // file.
639   std::string disk_image_path;
640   if (!MediaResidesOnDiskImage(media, &disk_image_path)) {
641     LOG(ERROR) << "MediaResidesOnDiskImage";
642     return;
643   }
644
645   // SynchronousDADiskUnmount and SynchronousDADiskEject require that the
646   // session be scheduled with the current run loop.
647   ScopedDASessionScheduleWithRunLoop session_run_loop(session,
648                                                       CFRunLoopGetCurrent(),
649                                                       kCFRunLoopCommonModes);
650
651   if (!SynchronousDADiskUnmount(disk, kDADiskUnmountOptionWhole)) {
652     LOG(ERROR) << "SynchronousDADiskUnmount";
653     return;
654   }
655
656   if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) {
657     LOG(ERROR) << "SynchronousDADiskEject";
658     return;
659   }
660
661   char* disk_image_path_in_trash_c;
662   OSStatus status = FSPathMoveObjectToTrashSync(disk_image_path.c_str(),
663                                                 &disk_image_path_in_trash_c,
664                                                 kFSFileOperationDefaultOptions);
665   if (status != noErr) {
666     OSSTATUS_LOG(ERROR, status) << "FSPathMoveObjectToTrashSync";
667     return;
668   }
669
670   // FSPathMoveObjectToTrashSync alone doesn't result in the Trash icon in the
671   // Dock indicating that any garbage has been placed within it. Using the
672   // trash path that FSPathMoveObjectToTrashSync claims to have used, call
673   // FNNotifyByPath to fatten up the icon.
674   base::FilePath disk_image_path_in_trash(disk_image_path_in_trash_c);
675   free(disk_image_path_in_trash_c);
676
677   base::FilePath trash_path = disk_image_path_in_trash.DirName();
678   const UInt8* trash_path_u8 = reinterpret_cast<const UInt8*>(
679       trash_path.value().c_str());
680   status = FNNotifyByPath(trash_path_u8,
681                           kFNDirectoryModifiedMessage,
682                           kNilOptions);
683   if (status != noErr) {
684     OSSTATUS_LOG(ERROR, status) << "FNNotifyByPath";
685     return;
686   }
687 }