| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/mac/install_from_dmg.h" |
| |
| #import <AppKit/AppKit.h> |
| #include <ApplicationServices/ApplicationServices.h> |
| #include <CoreFoundation/CoreFoundation.h> |
| #include <CoreServices/CoreServices.h> |
| #include <DiskArbitration/DiskArbitration.h> |
| #include <IOKit/IOKitLib.h> |
| #include <signal.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/param.h> |
| #include <sys/mount.h> |
| |
| #include "base/auto_reset.h" |
| #include "base/basictypes.h" |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/mac/authorization_util.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/mac_logging.h" |
| #include "base/mac/mach_logging.h" |
| #import "base/mac/mac_util.h" |
| #include "base/mac/scoped_authorizationref.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/mac/scoped_ioobject.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "chrome/browser/mac/dock.h" |
| #import "chrome/browser/mac/keystone_glue.h" |
| #include "chrome/browser/mac/relauncher.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "grit/chromium_strings.h" |
| #include "grit/generated_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| // When C++ exceptions are disabled, the C++ library defines |try| and |
| // |catch| so as to allow exception-expecting C++ code to build properly when |
| // language support for exceptions is not present. These macros interfere |
| // with the use of |@try| and |@catch| in Objective-C files such as this one. |
| // Undefine these macros here, after everything has been #included, since |
| // there will be no C++ uses and only Objective-C uses from this point on. |
| #undef try |
| #undef catch |
| |
| namespace { |
| |
| // Given an io_service_t (expected to be of class IOMedia), walks the ancestor |
| // chain, returning the closest ancestor that implements class IOHDIXHDDrive, |
| // if any. If no such ancestor is found, returns NULL. Following the "copy" |
| // rule, the caller assumes ownership of the returned value. |
| // |
| // Note that this looks for a class that inherits from IOHDIXHDDrive, but it |
| // will not likely find a concrete IOHDIXHDDrive. It will be |
| // IOHDIXHDDriveOutKernel for disk images mounted "out-of-kernel" or |
| // IOHDIXHDDriveInKernel for disk images mounted "in-kernel." Out-of-kernel is |
| // the default as of Mac OS X 10.5. See the documentation for "hdiutil attach |
| // -kernel" for more information. |
| io_service_t CopyHDIXDriveServiceForMedia(io_service_t media) { |
| const char disk_image_class[] = "IOHDIXHDDrive"; |
| |
| // This is highly unlikely. media as passed in is expected to be of class |
| // IOMedia. Since the media service's entire ancestor chain will be checked, |
| // though, check it as well. |
| if (IOObjectConformsTo(media, disk_image_class)) { |
| IOObjectRetain(media); |
| return media; |
| } |
| |
| io_iterator_t iterator_ref; |
| kern_return_t kr = |
| IORegistryEntryCreateIterator(media, |
| kIOServicePlane, |
| kIORegistryIterateRecursively | |
| kIORegistryIterateParents, |
| &iterator_ref); |
| if (kr != KERN_SUCCESS) { |
| MACH_LOG(ERROR, kr) << "IORegistryEntryCreateIterator"; |
| return IO_OBJECT_NULL; |
| } |
| base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref); |
| iterator_ref = IO_OBJECT_NULL; |
| |
| // Look at each of the ancestor services, beginning with the parent, |
| // iterating all the way up to the device tree's root. If any ancestor |
| // service matches the class used for disk images, the media resides on a |
| // disk image, and the disk image file's path can be determined by examining |
| // the image-path property. |
| for (base::mac::ScopedIOObject<io_service_t> ancestor( |
| IOIteratorNext(iterator)); |
| ancestor; |
| ancestor.reset(IOIteratorNext(iterator))) { |
| if (IOObjectConformsTo(ancestor, disk_image_class)) { |
| return ancestor.release(); |
| } |
| } |
| |
| // The media does not reside on a disk image. |
| return IO_OBJECT_NULL; |
| } |
| |
| // Given an io_service_t (expected to be of class IOMedia), determines whether |
| // that service is on a disk image. If it is, returns true. If image_path is |
| // present, it will be set to the pathname of the disk image file, encoded in |
| // filesystem encoding. |
| bool MediaResidesOnDiskImage(io_service_t media, std::string* image_path) { |
| if (image_path) { |
| image_path->clear(); |
| } |
| |
| base::mac::ScopedIOObject<io_service_t> hdix_drive( |
| CopyHDIXDriveServiceForMedia(media)); |
| if (!hdix_drive) { |
| return false; |
| } |
| |
| if (image_path) { |
| base::ScopedCFTypeRef<CFTypeRef> image_path_cftyperef( |
| IORegistryEntryCreateCFProperty( |
| hdix_drive, CFSTR("image-path"), NULL, 0)); |
| if (!image_path_cftyperef) { |
| LOG(ERROR) << "IORegistryEntryCreateCFProperty"; |
| return true; |
| } |
| if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) { |
| base::ScopedCFTypeRef<CFStringRef> observed_type_cf( |
| CFCopyTypeIDDescription(CFGetTypeID(image_path_cftyperef))); |
| std::string observed_type; |
| if (observed_type_cf) { |
| observed_type.assign(", observed "); |
| observed_type.append(base::SysCFStringRefToUTF8(observed_type_cf)); |
| } |
| LOG(ERROR) << "image-path: expected CFData, observed " << observed_type; |
| return true; |
| } |
| |
| CFDataRef image_path_data = static_cast<CFDataRef>( |
| image_path_cftyperef.get()); |
| CFIndex length = CFDataGetLength(image_path_data); |
| if (length <= 0) { |
| LOG(ERROR) << "image_path_data is unexpectedly empty"; |
| return true; |
| } |
| char* image_path_c = WriteInto(image_path, length + 1); |
| CFDataGetBytes(image_path_data, |
| CFRangeMake(0, length), |
| reinterpret_cast<UInt8*>(image_path_c)); |
| } |
| |
| return true; |
| } |
| |
| // Returns true if |path| is located on a read-only filesystem of a disk |
| // image. Returns false if not, or in the event of an error. If |
| // out_dmg_bsd_device_name is present, it will be set to the BSD device name |
| // for the disk image's device, in "diskNsM" form. |
| bool IsPathOnReadOnlyDiskImage(const char path[], |
| std::string* out_dmg_bsd_device_name) { |
| if (out_dmg_bsd_device_name) { |
| out_dmg_bsd_device_name->clear(); |
| } |
| |
| struct statfs statfs_buf; |
| if (statfs(path, &statfs_buf) != 0) { |
| PLOG(ERROR) << "statfs " << path; |
| return false; |
| } |
| |
| if (!(statfs_buf.f_flags & MNT_RDONLY)) { |
| // Not on a read-only filesystem. |
| return false; |
| } |
| |
| const char dev_root[] = "/dev/"; |
| const int dev_root_length = arraysize(dev_root) - 1; |
| if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) { |
| // Not rooted at dev_root, no BSD name to search on. |
| return false; |
| } |
| |
| // BSD names in IOKit don't include dev_root. |
| const char* dmg_bsd_device_name = statfs_buf.f_mntfromname + dev_root_length; |
| if (out_dmg_bsd_device_name) { |
| out_dmg_bsd_device_name->assign(dmg_bsd_device_name); |
| } |
| |
| const mach_port_t master_port = kIOMasterPortDefault; |
| |
| // IOBSDNameMatching gives ownership of match_dict to the caller, but |
| // IOServiceGetMatchingServices will assume that reference. |
| CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port, |
| 0, |
| dmg_bsd_device_name); |
| if (!match_dict) { |
| LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name; |
| return false; |
| } |
| |
| io_iterator_t iterator_ref; |
| kern_return_t kr = IOServiceGetMatchingServices(master_port, |
| match_dict, |
| &iterator_ref); |
| if (kr != KERN_SUCCESS) { |
| MACH_LOG(ERROR, kr) << "IOServiceGetMatchingServices"; |
| return false; |
| } |
| base::mac::ScopedIOObject<io_iterator_t> iterator(iterator_ref); |
| iterator_ref = IO_OBJECT_NULL; |
| |
| // There needs to be exactly one matching service. |
| base::mac::ScopedIOObject<io_service_t> media(IOIteratorNext(iterator)); |
| if (!media) { |
| LOG(ERROR) << "IOIteratorNext: no service"; |
| return false; |
| } |
| base::mac::ScopedIOObject<io_service_t> unexpected_service( |
| IOIteratorNext(iterator)); |
| if (unexpected_service) { |
| LOG(ERROR) << "IOIteratorNext: too many services"; |
| return false; |
| } |
| |
| iterator.reset(); |
| |
| return MediaResidesOnDiskImage(media, NULL); |
| } |
| |
| // Returns true if the application is located on a read-only filesystem of a |
| // disk image. Returns false if not, or in the event of an error. If |
| // dmg_bsd_device_name is present, it will be set to the BSD device name for |
| // the disk image's device, in "diskNsM" form. |
| bool IsAppRunningFromReadOnlyDiskImage(std::string* dmg_bsd_device_name) { |
| return IsPathOnReadOnlyDiskImage( |
| [[base::mac::OuterBundle() bundlePath] fileSystemRepresentation], |
| dmg_bsd_device_name); |
| } |
| |
| // Shows a dialog asking the user whether or not to install from the disk |
| // image. Returns true if the user approves installation. |
| bool ShouldInstallDialog() { |
| NSString* title = l10n_util::GetNSStringFWithFixup( |
| IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); |
| NSString* prompt = l10n_util::GetNSStringFWithFixup( |
| IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); |
| NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES); |
| NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO); |
| |
| NSAlert* alert = [[[NSAlert alloc] init] autorelease]; |
| |
| [alert setAlertStyle:NSInformationalAlertStyle]; |
| [alert setMessageText:title]; |
| [alert setInformativeText:prompt]; |
| [alert addButtonWithTitle:yes]; |
| NSButton* cancel_button = [alert addButtonWithTitle:no]; |
| [cancel_button setKeyEquivalent:@"\e"]; |
| |
| NSInteger result = [alert runModal]; |
| |
| return result == NSAlertFirstButtonReturn; |
| } |
| |
| // Potentially shows an authorization dialog to request authentication to |
| // copy. If application_directory appears to be unwritable, attempts to |
| // obtain authorization, which may result in the display of the dialog. |
| // Returns NULL if authorization is not performed because it does not appear |
| // to be necessary because the user has permission to write to |
| // application_directory. Returns NULL if authorization fails. |
| AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) { |
| NSFileManager* file_manager = [NSFileManager defaultManager]; |
| if ([file_manager isWritableFileAtPath:application_directory]) { |
| return NULL; |
| } |
| |
| NSString* prompt = l10n_util::GetNSStringFWithFixup( |
| IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT, |
| l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); |
| return base::mac::AuthorizationCreateToRunAsRoot( |
| base::mac::NSToCFCast(prompt)); |
| } |
| |
| // Invokes the installer program at installer_path to copy source_path to |
| // target_path and perform any additional on-disk bookkeeping needed to be |
| // able to launch target_path properly. If authorization_arg is non-NULL, |
| // function will assume ownership of it, will invoke the installer with that |
| // authorization reference, and will attempt Keystone ticket promotion. |
| bool InstallFromDiskImage(AuthorizationRef authorization_arg, |
| NSString* installer_path, |
| NSString* source_path, |
| NSString* target_path) { |
| base::mac::ScopedAuthorizationRef authorization(authorization_arg); |
| authorization_arg = NULL; |
| int exit_status; |
| if (authorization) { |
| const char* installer_path_c = [installer_path fileSystemRepresentation]; |
| const char* source_path_c = [source_path fileSystemRepresentation]; |
| const char* target_path_c = [target_path fileSystemRepresentation]; |
| const char* arguments[] = {source_path_c, target_path_c, NULL}; |
| |
| OSStatus status = base::mac::ExecuteWithPrivilegesAndWait( |
| authorization, |
| installer_path_c, |
| kAuthorizationFlagDefaults, |
| arguments, |
| NULL, // pipe |
| &exit_status); |
| if (status != errAuthorizationSuccess) { |
| OSSTATUS_LOG(ERROR, status) |
| << "AuthorizationExecuteWithPrivileges install"; |
| return false; |
| } |
| } else { |
| NSArray* arguments = [NSArray arrayWithObjects:source_path, |
| target_path, |
| nil]; |
| |
| NSTask* task; |
| @try { |
| task = [NSTask launchedTaskWithLaunchPath:installer_path |
| arguments:arguments]; |
| } @catch(NSException* exception) { |
| LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: " |
| << [[exception description] UTF8String]; |
| return false; |
| } |
| |
| [task waitUntilExit]; |
| exit_status = [task terminationStatus]; |
| } |
| |
| if (exit_status != 0) { |
| LOG(ERROR) << "install.sh: exit status " << exit_status; |
| return false; |
| } |
| |
| if (authorization) { |
| // As long as an AuthorizationRef is available, promote the Keystone |
| // ticket. Inform KeystoneGlue of the new path to use. |
| KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue]; |
| [keystone_glue setAppPath:target_path]; |
| [keystone_glue promoteTicketWithAuthorization:authorization.release() |
| synchronous:YES]; |
| } |
| |
| return true; |
| } |
| |
| // Launches the application at installed_path. The helper application |
| // contained within install_path will be used for the relauncher process. This |
| // keeps Launch Services from ever having to see or think about the helper |
| // application on the disk image. The relauncher process will be asked to |
| // call EjectAndTrashDiskImage on dmg_bsd_device_name. |
| bool LaunchInstalledApp(NSString* installed_path, |
| const std::string& dmg_bsd_device_name) { |
| base::FilePath browser_path([installed_path fileSystemRepresentation]); |
| |
| base::FilePath helper_path = browser_path.Append("Contents/Versions"); |
| helper_path = helper_path.Append(chrome::kChromeVersion); |
| helper_path = helper_path.Append(chrome::kHelperProcessExecutablePath); |
| |
| std::vector<std::string> args = |
| CommandLine::ForCurrentProcess()->argv(); |
| args[0] = browser_path.value(); |
| |
| std::vector<std::string> relauncher_args; |
| if (!dmg_bsd_device_name.empty()) { |
| std::string dmg_arg(mac_relauncher::kRelauncherDMGDeviceArg); |
| dmg_arg.append(dmg_bsd_device_name); |
| relauncher_args.push_back(dmg_arg); |
| } |
| |
| return mac_relauncher::RelaunchAppWithHelper(helper_path.value(), |
| relauncher_args, |
| args); |
| } |
| |
| void ShowErrorDialog() { |
| NSString* title = l10n_util::GetNSStringWithFixup( |
| IDS_INSTALL_FROM_DMG_ERROR_TITLE); |
| NSString* error = l10n_util::GetNSStringFWithFixup( |
| IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); |
| NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK); |
| |
| NSAlert* alert = [[[NSAlert alloc] init] autorelease]; |
| |
| [alert setAlertStyle:NSWarningAlertStyle]; |
| [alert setMessageText:title]; |
| [alert setInformativeText:error]; |
| [alert addButtonWithTitle:ok]; |
| |
| [alert runModal]; |
| } |
| |
| } // namespace |
| |
| bool MaybeInstallFromDiskImage() { |
| base::mac::ScopedNSAutoreleasePool autorelease_pool; |
| |
| std::string dmg_bsd_device_name; |
| if (!IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name)) { |
| return false; |
| } |
| |
| NSArray* application_directories = |
| NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, |
| NSLocalDomainMask, |
| YES); |
| if ([application_directories count] == 0) { |
| LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: " |
| << "no local application directories"; |
| return false; |
| } |
| NSString* application_directory = [application_directories objectAtIndex:0]; |
| |
| NSFileManager* file_manager = [NSFileManager defaultManager]; |
| |
| BOOL is_directory; |
| if (![file_manager fileExistsAtPath:application_directory |
| isDirectory:&is_directory] || |
| !is_directory) { |
| VLOG(1) << "No application directory at " |
| << [application_directory UTF8String]; |
| return false; |
| } |
| |
| NSString* source_path = [base::mac::OuterBundle() bundlePath]; |
| NSString* application_name = [source_path lastPathComponent]; |
| NSString* target_path = |
| [application_directory stringByAppendingPathComponent:application_name]; |
| |
| if ([file_manager fileExistsAtPath:target_path]) { |
| VLOG(1) << "Something already exists at " << [target_path UTF8String]; |
| return false; |
| } |
| |
| NSString* installer_path = |
| [base::mac::FrameworkBundle() pathForResource:@"install" ofType:@"sh"]; |
| if (!installer_path) { |
| VLOG(1) << "Could not locate install.sh"; |
| return false; |
| } |
| |
| if (!ShouldInstallDialog()) { |
| return false; |
| } |
| |
| base::mac::ScopedAuthorizationRef authorization( |
| MaybeShowAuthorizationDialog(application_directory)); |
| // authorization will be NULL if it's deemed unnecessary or if |
| // authentication fails. In either case, try to install without privilege |
| // escalation. |
| |
| if (!InstallFromDiskImage(authorization.release(), |
| installer_path, |
| source_path, |
| target_path)) { |
| ShowErrorDialog(); |
| return false; |
| } |
| |
| dock::AddIcon(target_path, source_path); |
| |
| if (dmg_bsd_device_name.empty()) { |
| // Not fatal, just diagnostic. |
| LOG(ERROR) << "Could not determine disk image BSD device name"; |
| } |
| |
| if (!LaunchInstalledApp(target_path, dmg_bsd_device_name)) { |
| ShowErrorDialog(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| namespace { |
| |
| // A simple scoper that calls DASessionScheduleWithRunLoop when created and |
| // DASessionUnscheduleFromRunLoop when destroyed. |
| class ScopedDASessionScheduleWithRunLoop { |
| public: |
| ScopedDASessionScheduleWithRunLoop(DASessionRef session, |
| CFRunLoopRef run_loop, |
| CFStringRef run_loop_mode) |
| : session_(session), |
| run_loop_(run_loop), |
| run_loop_mode_(run_loop_mode) { |
| DASessionScheduleWithRunLoop(session_, run_loop_, run_loop_mode_); |
| } |
| |
| ~ScopedDASessionScheduleWithRunLoop() { |
| DASessionUnscheduleFromRunLoop(session_, run_loop_, run_loop_mode_); |
| } |
| |
| private: |
| DASessionRef session_; |
| CFRunLoopRef run_loop_; |
| CFStringRef run_loop_mode_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScopedDASessionScheduleWithRunLoop); |
| }; |
| |
| // A small structure used to ferry data between SynchronousDAOperation and |
| // SynchronousDACallbackAdapter. |
| struct SynchronousDACallbackData { |
| public: |
| SynchronousDACallbackData() |
| : callback_called(false), |
| run_loop_running(false) { |
| } |
| |
| base::ScopedCFTypeRef<DADissenterRef> dissenter; |
| bool callback_called; |
| bool run_loop_running; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(SynchronousDACallbackData); |
| }; |
| |
| // The callback target for SynchronousDAOperation. Set the fields in |
| // SynchronousDACallbackData properly and then stops the run loop so that |
| // SynchronousDAOperation may proceed. |
| void SynchronousDACallbackAdapter(DADiskRef disk, |
| DADissenterRef dissenter, |
| void* context) { |
| SynchronousDACallbackData* callback_data = |
| static_cast<SynchronousDACallbackData*>(context); |
| callback_data->callback_called = true; |
| |
| if (dissenter) { |
| CFRetain(dissenter); |
| callback_data->dissenter.reset(dissenter); |
| } |
| |
| // Only stop the run loop if SynchronousDAOperation started it. Don't stop |
| // anything if this callback was reached synchronously from DADiskUnmount or |
| // DADiskEject. |
| if (callback_data->run_loop_running) { |
| CFRunLoopStop(CFRunLoopGetCurrent()); |
| } |
| } |
| |
| // Performs a DiskArbitration operation synchronously. After the operation is |
| // requested by SynchronousDADiskUnmount or SynchronousDADiskEject, those |
| // functions will call this one to run a run loop for a period of time, |
| // waiting for the callback to be called. When the callback is called, the |
| // run loop will be stopped, and this function will examine the result. If |
| // a dissenter prevented the operation from completing, or if the run loop |
| // timed out without the callback being called, this function will return |
| // false. When the callback completes successfully with no dissenters within |
| // the time allotted, this function returns true. This function requires that |
| // the DASession being used for the operation being performed has been added |
| // to the current run loop with DASessionScheduleWithRunLoop. |
| bool SynchronousDAOperation(const char* name, |
| SynchronousDACallbackData* callback_data) { |
| // The callback may already have been called synchronously. In that case, |
| // avoid spinning the run loop at all. |
| if (!callback_data->callback_called) { |
| const CFTimeInterval kOperationTimeoutSeconds = 15; |
| base::AutoReset<bool> running_reset(&callback_data->run_loop_running, true); |
| CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE); |
| } |
| |
| if (!callback_data->callback_called) { |
| LOG(ERROR) << name << ": timed out"; |
| return false; |
| } else if (callback_data->dissenter) { |
| CFStringRef status_string_cf = |
| DADissenterGetStatusString(callback_data->dissenter); |
| std::string status_string; |
| if (status_string_cf) { |
| status_string.assign(" "); |
| status_string.append(base::SysCFStringRefToUTF8(status_string_cf)); |
| } |
| LOG(ERROR) << name << ": dissenter: " |
| << DADissenterGetStatus(callback_data->dissenter) |
| << status_string; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Calls DADiskUnmount synchronously, returning the result. |
| bool SynchronousDADiskUnmount(DADiskRef disk, DADiskUnmountOptions options) { |
| SynchronousDACallbackData callback_data; |
| DADiskUnmount(disk, options, SynchronousDACallbackAdapter, &callback_data); |
| return SynchronousDAOperation("DADiskUnmount", &callback_data); |
| } |
| |
| // Calls DADiskEject synchronously, returning the result. |
| bool SynchronousDADiskEject(DADiskRef disk, DADiskEjectOptions options) { |
| SynchronousDACallbackData callback_data; |
| DADiskEject(disk, options, SynchronousDACallbackAdapter, &callback_data); |
| return SynchronousDAOperation("DADiskEject", &callback_data); |
| } |
| |
| } // namespace |
| |
| void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) { |
| base::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL)); |
| if (!session.get()) { |
| LOG(ERROR) << "DASessionCreate"; |
| return; |
| } |
| |
| base::ScopedCFTypeRef<DADiskRef> disk( |
| DADiskCreateFromBSDName(NULL, session, dmg_bsd_device_name.c_str())); |
| if (!disk.get()) { |
| LOG(ERROR) << "DADiskCreateFromBSDName"; |
| return; |
| } |
| |
| // dmg_bsd_device_name may only refer to part of the disk: it may be a |
| // single filesystem on a larger disk. Use the "whole disk" object to |
| // be able to unmount all mounted filesystems from the disk image, and eject |
| // the image. This is harmless if dmg_bsd_device_name already referred to a |
| // "whole disk." |
| disk.reset(DADiskCopyWholeDisk(disk)); |
| if (!disk.get()) { |
| LOG(ERROR) << "DADiskCopyWholeDisk"; |
| return; |
| } |
| |
| base::mac::ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk)); |
| if (!media.get()) { |
| LOG(ERROR) << "DADiskCopyIOMedia"; |
| return; |
| } |
| |
| // Make sure the device is a disk image, and get the path to its disk image |
| // file. |
| std::string disk_image_path; |
| if (!MediaResidesOnDiskImage(media, &disk_image_path)) { |
| LOG(ERROR) << "MediaResidesOnDiskImage"; |
| return; |
| } |
| |
| // SynchronousDADiskUnmount and SynchronousDADiskEject require that the |
| // session be scheduled with the current run loop. |
| ScopedDASessionScheduleWithRunLoop session_run_loop(session, |
| CFRunLoopGetCurrent(), |
| kCFRunLoopCommonModes); |
| |
| if (!SynchronousDADiskUnmount(disk, kDADiskUnmountOptionWhole)) { |
| LOG(ERROR) << "SynchronousDADiskUnmount"; |
| return; |
| } |
| |
| if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) { |
| LOG(ERROR) << "SynchronousDADiskEject"; |
| return; |
| } |
| |
| char* disk_image_path_in_trash_c; |
| OSStatus status = FSPathMoveObjectToTrashSync(disk_image_path.c_str(), |
| &disk_image_path_in_trash_c, |
| kFSFileOperationDefaultOptions); |
| if (status != noErr) { |
| OSSTATUS_LOG(ERROR, status) << "FSPathMoveObjectToTrashSync"; |
| return; |
| } |
| |
| // FSPathMoveObjectToTrashSync alone doesn't result in the Trash icon in the |
| // Dock indicating that any garbage has been placed within it. Using the |
| // trash path that FSPathMoveObjectToTrashSync claims to have used, call |
| // FNNotifyByPath to fatten up the icon. |
| base::FilePath disk_image_path_in_trash(disk_image_path_in_trash_c); |
| free(disk_image_path_in_trash_c); |
| |
| base::FilePath trash_path = disk_image_path_in_trash.DirName(); |
| const UInt8* trash_path_u8 = reinterpret_cast<const UInt8*>( |
| trash_path.value().c_str()); |
| status = FNNotifyByPath(trash_path_u8, |
| kFNDirectoryModifiedMessage, |
| kNilOptions); |
| if (status != noErr) { |
| OSSTATUS_LOG(ERROR, status) << "FNNotifyByPath"; |
| return; |
| } |
| } |