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