Home | History | Annotate | Download | only in mac
      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 }
    688