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