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