• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/param.h>
17#include <sys/mount.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#include "base/mac/mach_logging.h"
28#import "base/mac/mac_util.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 "grit/chromium_strings.h"
40#include "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
53namespace {
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.
66io_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.
113bool 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.
164bool 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.
239bool 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.
247bool 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.
275AuthorizationRef 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.
293bool 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.
359bool 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
383void 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
402bool 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
482namespace {
483
484// A simple scoper that calls DASessionScheduleWithRunLoop when created and
485// DASessionUnscheduleFromRunLoop when destroyed.
486class 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.
511struct 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.
529void 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.
560bool 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.
591bool 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.
598bool 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
606void 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