Chromium Code Reviews| Index: chrome/browser/mac/install_from_dmg.mm |
| =================================================================== |
| --- chrome/browser/mac/install_from_dmg.mm (revision 90648) |
| +++ chrome/browser/mac/install_from_dmg.mm (working copy) |
| @@ -8,7 +8,9 @@ |
| #import <AppKit/AppKit.h> |
| #include <CoreFoundation/CoreFoundation.h> |
| #include <CoreServices/CoreServices.h> |
| +#include <DiskArbitration/DiskArbitration.h> |
| #include <IOKit/IOKitLib.h> |
| +#include <stdlib.h> |
| #include <string.h> |
| #include <sys/param.h> |
| #include <sys/mount.h> |
| @@ -18,7 +20,10 @@ |
| #include "base/file_path.h" |
| #include "base/logging.h" |
| #import "base/mac/mac_util.h" |
| +#include "base/mac/scoped_cftyperef.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| +#include "base/string_util.h" |
| +#include "base/sys_string_conversions.h" |
| #include "chrome/browser/mac/authorization_util.h" |
| #include "chrome/browser/mac/scoped_authorizationref.h" |
| #include "chrome/browser/mac/scoped_ioobject.h" |
| @@ -41,9 +46,105 @@ |
| 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. |
| +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) { |
| + LOG(ERROR) << "IORegistryEntryCreateIterator :" << kr; |
| + return NULL; |
| + } |
| + ScopedIOObject<io_iterator_t> iterator(iterator_ref); |
| + iterator_ref = 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 (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 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 to disk image file. |
|
Avi (use Gerrit)
2011/06/27 23:28:09
With what encoding? Unlike device paths, disk imag
|
| +bool MediaResidesOnDiskImage(io_service_t media, std::string* image_path) { |
| + if (image_path) { |
| + image_path->clear(); |
| + } |
| + |
| + ScopedIOObject<io_service_t> hdix_drive(CopyHDIXDriveServiceForMedia(media)); |
| + if (!hdix_drive) { |
| + return false; |
| + } |
| + |
| + if (image_path) { |
| + base::mac::ScopedCFTypeRef<CFTypeRef> image_path_cftyperef( |
| + IORegistryEntryCreateCFProperty(hdix_drive, |
| + CFSTR("image-path"), |
| + NULL, |
| + 0)); |
| + if (!image_path_cftyperef) { |
| + LOG(WARNING) << "IORegistryEntryCreateCFProperty"; |
| + return true; |
| + } |
| + if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) { |
| + base::mac::ScopedCFTypeRef<CFStringRef> observed_type( |
| + CFCopyTypeIDDescription(CFGetTypeID(image_path_cftyperef))); |
| + LOG(WARNING) << "image-path: expected NSData, observed " |
|
Avi (use Gerrit)
2011/06/27 23:28:09
well.... you expected a CFData, not an NSData.
|
| + << base::SysCFStringRefToUTF8(observed_type); |
| + return true; |
| + } |
| + |
| + CFDataRef image_path_data = static_cast<CFDataRef>( |
| + image_path_cftyperef.get()); |
| + CFIndex length = CFDataGetLength(image_path_data); |
| + 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. |
| -bool IsPathOnReadOnlyDiskImage(const char path[]) { |
| +// 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; |
| @@ -63,7 +164,10 @@ |
| } |
| // BSD names in IOKit don't include dev_root. |
| - const char* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length; |
| + 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; |
| @@ -71,9 +175,9 @@ |
| // IOServiceGetMatchingServices will assume that reference. |
| CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port, |
| 0, |
| - bsd_device_name); |
| + dmg_bsd_device_name); |
| if (!match_dict) { |
| - LOG(ERROR) << "IOBSDNameMatching " << bsd_device_name; |
| + LOG(ERROR) << "IOBSDNameMatching " << dmg_bsd_device_name; |
| return false; |
| } |
| @@ -82,7 +186,7 @@ |
| match_dict, |
| &iterator_ref); |
| if (kr != KERN_SUCCESS) { |
| - LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name |
| + LOG(ERROR) << "IOServiceGetMatchingServices " << dmg_bsd_device_name |
| << ": kernel error " << kr; |
| return false; |
| } |
| @@ -90,62 +194,31 @@ |
| iterator_ref = NULL; |
| // There needs to be exactly one matching service. |
| - ScopedIOObject<io_service_t> filesystem_service(IOIteratorNext(iterator)); |
| - if (!filesystem_service) { |
| - LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service"; |
| + ScopedIOObject<io_service_t> media(IOIteratorNext(iterator)); |
| + if (!media) { |
| + LOG(ERROR) << "IOIteratorNext " << dmg_bsd_device_name << ": no service"; |
| return false; |
| } |
| ScopedIOObject<io_service_t> unexpected_service(IOIteratorNext(iterator)); |
| if (unexpected_service) { |
| - LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": too many services"; |
| + LOG(ERROR) << "IOIteratorNext " << dmg_bsd_device_name |
| + << ": too many services"; |
| return false; |
| } |
| iterator.reset(); |
| - const char disk_image_class[] = "IOHDIXController"; |
| - |
| - // This is highly unlikely. The filesystem service is expected to be of |
| - // class IOMedia. Since the filesystem service's entire ancestor chain |
| - // will be checked, though, check the filesystem service's class itself. |
| - if (IOObjectConformsTo(filesystem_service, disk_image_class)) { |
| - return true; |
| - } |
| - |
| - kr = IORegistryEntryCreateIterator(filesystem_service, |
| - kIOServicePlane, |
| - kIORegistryIterateRecursively | |
| - kIORegistryIterateParents, |
| - &iterator_ref); |
| - if (kr != KERN_SUCCESS) { |
| - LOG(ERROR) << "IORegistryEntryCreateIterator " << bsd_device_name |
| - << ": kernel error " << kr; |
| - return false; |
| - } |
| - iterator.reset(iterator_ref); |
| - iterator_ref = NULL; |
| - |
| - // Look at each of the filesystem service's 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 |
| - // filesystem resides on a disk image. |
| - for (ScopedIOObject<io_service_t> ancestor_service(IOIteratorNext(iterator)); |
| - ancestor_service; |
| - ancestor_service.reset(IOIteratorNext(iterator))) { |
| - if (IOObjectConformsTo(ancestor_service, disk_image_class)) { |
| - return true; |
| - } |
| - } |
| - |
| - // The filesystem does not reside on a disk image. |
| - return false; |
| + 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. |
| -bool IsAppRunningFromReadOnlyDiskImage() { |
| +// 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( |
| - [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]); |
| + [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation], |
| + dmg_bsd_device_name); |
| } |
| // Shows a dialog asking the user whether or not to install from the disk |
| @@ -259,8 +332,10 @@ |
| // 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. |
| -bool LaunchInstalledApp(NSString* installed_path) { |
| +// 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) { |
| FilePath browser_path([installed_path fileSystemRepresentation]); |
| FilePath helper_path = browser_path.Append("Contents/Versions"); |
| @@ -271,7 +346,16 @@ |
| CommandLine::ForCurrentProcess()->argv(); |
| args[0] = browser_path.value(); |
| - return mac_relauncher::RelaunchAppWithHelper(helper_path.value(), args); |
| + std::vector<std::string> relauncher_args; |
| + if (!dmg_bsd_device_name.empty()) { |
| + std::string dmg_arg(mac_relauncher::kRelauncherDMGArg); |
| + 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() { |
| @@ -296,7 +380,8 @@ |
| bool MaybeInstallFromDiskImage() { |
| base::mac::ScopedNSAutoreleasePool autorelease_pool; |
| - if (!IsAppRunningFromReadOnlyDiskImage()) { |
| + std::string dmg_bsd_device_name; |
| + if (!IsAppRunningFromReadOnlyDiskImage(&dmg_bsd_device_name)) { |
| return false; |
| } |
| @@ -353,10 +438,198 @@ |
| installer_path, |
| source_path, |
| target_path) || |
| - !LaunchInstalledApp(target_path)) { |
| + !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) {} |
| + |
| + base::mac::ScopedCFTypeRef<DADissenterRef> dissenter; |
| + bool callback_called; |
| + |
| + 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); |
| + } |
| + |
| + 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) { |
| + const CFTimeInterval kOperationTimeoutSeconds = 15; |
| + CFRunLoopRunInMode(kCFRunLoopDefaultMode, kOperationTimeoutSeconds, FALSE); |
| + |
| + if (!callback_data->callback_called) { |
| + LOG(WARNING) << name << ": timed out"; |
| + return false; |
| + } else if (callback_data->dissenter) { |
| + CFStringRef status_string = |
| + DADissenterGetStatusString(callback_data->dissenter); |
| + LOG(WARNING) << name << ": dissenter: " |
| + << DADissenterGetStatus(callback_data->dissenter) << " " |
| + << base::SysCFStringRefToUTF8(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); |
|
Avi (use Gerrit)
2011/06/27 23:28:09
Does this work in the general case? In certain err
|
| + 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); |
|
Avi (use Gerrit)
2011/06/27 23:28:09
ditto
|
| +} |
| + |
| +} // namespace |
| + |
| +void EjectAndTrashDiskImage(const std::string& dmg_bsd_device_name) { |
| + base::mac::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL)); |
| + if (!session.get()) { |
| + LOG(WARNING) << "DASessionCreate"; |
| + return; |
| + } |
| + |
| + base::mac::ScopedCFTypeRef<DADiskRef> disk( |
| + DADiskCreateFromBSDName(NULL, session, dmg_bsd_device_name.c_str())); |
| + if (!disk.get()) { |
| + LOG(WARNING) << "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(WARNING) << "DADiskCopyWholeDisk"; |
| + return; |
| + } |
| + |
| + ScopedIOObject<io_service_t> media(DADiskCopyIOMedia(disk)); |
| + if (!media.get()) { |
| + LOG(WARNING) << "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(WARNING) << "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(WARNING) << "SynchronousDADiskUnmount"; |
| + return; |
| + } |
| + |
| + if (!SynchronousDADiskEject(disk, kDADiskEjectOptionDefault)) { |
| + LOG(WARNING) << "SynchronousDADiskEject"; |
| + return; |
| + } |
| + |
| + char* disk_image_path_in_trash_c; |
| + OSStatus status = FSPathMoveObjectToTrashSync(disk_image_path.c_str(), |
|
Avi (use Gerrit)
2011/06/27 23:28:09
Is disk_image_path UTF-8? See my earlier comment.
|
| + &disk_image_path_in_trash_c, |
| + kFSFileOperationDefaultOptions); |
| + if (status != noErr) { |
| + LOG(WARNING) << "FSPathMoveObjectToTrashSync: " << status; |
| + 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. |
| + FilePath disk_image_path_in_trash(disk_image_path_in_trash_c); |
| + free(disk_image_path_in_trash_c); |
| + |
| + 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) { |
| + LOG(WARNING) << "FNNotifyByPath: " << status; |
| + return; |
| + } |
| +} |