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 #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" 42 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 51 52 namespace { 53 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. 58 // 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"; 67 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 } 75 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; 89 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 } 103 104 // The media does not reside on a disk image. 105 return IO_OBJECT_NULL; 106 } 107 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 } 116 117 base::mac::ScopedIOObject<io_service_t> hdix_drive( 118 CopyHDIXDriveServiceForMedia(media)); 119 if (!hdix_drive) { 120 return false; 121 } 122 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 } 142 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 } 155 156 return true; 157 } 158 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 } 168 169 struct statfs statfs_buf; 170 if (statfs(path, &statfs_buf) != 0) { 171 PLOG(ERROR) << "statfs " << path; 172 return false; 173 } 174 175 if (!(statfs_buf.f_flags & MNT_RDONLY)) { 176 // Not on a read-only filesystem. 177 return false; 178 } 179 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 } 186 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 } 192 193 const mach_port_t master_port = kIOMasterPortDefault; 194 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 } 204 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; 215 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 } 228 229 iterator.reset(); 230 231 return MediaResidesOnDiskImage(media, NULL); 232 } 233 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); 242 } 243 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); 253 254 NSAlert* alert = [[[NSAlert alloc] init] autorelease]; 255 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"]; 262 263 NSInteger result = [alert runModal]; 264 265 return result == NSAlertFirstButtonReturn; 266 } 267 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 } 279 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)); 285 } 286 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}; 304 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]; 321 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 } 331 332 [task waitUntilExit]; 333 exit_status = [task terminationStatus]; 334 } 335 336 if (exit_status != 0) { 337 LOG(ERROR) << "install.sh: exit status " << exit_status; 338 return false; 339 } 340 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 } 349 350 return true; 351 } 352 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]); 361 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); 365 366 std::vector<std::string> args = 367 CommandLine::ForCurrentProcess()->argv(); 368 args[0] = browser_path.value(); 369 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 } 376 377 return mac_relauncher::RelaunchAppWithHelper(helper_path.value(), 378 relauncher_args, 379 args); 380 } 381 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); 388 389 NSAlert* alert = [[[NSAlert alloc] init] autorelease]; 390 391 [alert setAlertStyle:NSWarningAlertStyle]; 392 [alert setMessageText:title]; 393 [alert setInformativeText:error]; 394 [alert addButtonWithTitle:ok]; 395 396 [alert runModal]; 397 } 398 399 } // namespace 400 401 bool MaybeInstallFromDiskImage() { 402 base::mac::ScopedNSAutoreleasePool autorelease_pool; 403 404 std::string dmg_bsd_device_name; 405 if (!IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name)) { 406 return false; 407 } 408 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]; 419 420 NSFileManager* file_manager = [NSFileManager defaultManager]; 421 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 } 430 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]; 435 436 if ([file_manager fileExistsAtPath:target_path]) { 437 VLOG(1) << "Something already exists at " << [target_path UTF8String]; 438 return false; 439 } 440 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 } 447 448 if (!ShouldInstallDialog()) { 449 return false; 450 } 451 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. 457 458 if (!InstallFromDiskImage(authorization.release(), 459 installer_path, 460 source_path, 461 target_path)) { 462 ShowErrorDialog(); 463 return false; 464 } 465 466 dock::AddIcon(target_path, source_path); 467 468 if (dmg_bsd_device_name.empty()) { 469 // Not fatal, just diagnostic. 470 LOG(ERROR) << "Could not determine disk image BSD device name"; 471 } 472 473 if (!LaunchInstalledApp(target_path, dmg_bsd_device_name)) { 474 ShowErrorDialog(); 475 return false; 476 } 477 478 return true; 479 } 480 481 namespace { 482 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 } 495 496 ~ScopedDASessionScheduleWithRunLoop() { 497 DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_); 498 } 499 500 private: 501 DASessionRef session_; 502 CFRunLoopRef run_loop_; 503 CFStringRef run_loop_mode_; 504 505 DISALLOW_COPY_AND_ASSIGN(ScopedDASessionScheduleWithRunLoop); 506 }; 507 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 } 516 517 base::ScopedCFTypeRef<DADissenterRef> dissenter; 518 bool callback_called; 519 bool run_loop_running; 520 521 private: 522 DISALLOW_COPY_AND_ASSIGN(SynchronousDACallbackData); 523 }; 524 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; 534 535 if (dissenter) { 536 CFRetain(dissenter); 537 callback_data->dissenter.reset(dissenter); 538 } 539 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 } 546 } 547 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 } 568 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 } 585 586 return true; 587 } 588 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); 594 } 595 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); 601 } 602 603 } // namespace 604 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 } 611 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 } 618 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 } 629 630 base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk)); 631 if (!media.get()) { 632 LOG(ERROR) << "DADiskCopyIOMedia"; 633 return; 634 } 635 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 } 643 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); 649 650 if (!SynchronousDADiskUnmount(disk, kDADiskUnmountOptionWhole)) { 651 LOG(ERROR) << "SynchronousDADiskUnmount"; 652 return; 653 } 654 655 if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) { 656 LOG(ERROR) << "SynchronousDADiskEject"; 657 return; 658 } 659 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 } 668 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); 675 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 } 686 } 687