| Index: chrome/browser/mac/install_from_dmg.mm
|
| ===================================================================
|
| --- chrome/browser/mac/install_from_dmg.mm (revision 90773)
|
| +++ chrome/browser/mac/install_from_dmg.mm (working copy)
|
| @@ -8,17 +8,23 @@
|
| #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>
|
|
|
| +#include "base/auto_reset.h"
|
| #include "base/basictypes.h"
|
| #include "base/command_line.h"
|
| #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 +47,117 @@
|
|
|
| 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) {
|
| + 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 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();
|
| + }
|
| +
|
| + 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(ERROR) << "IORegistryEntryCreateCFProperty";
|
| + return true;
|
| + }
|
| + if (CFGetTypeID(image_path_cftyperef) != CFDataGetTypeID()) {
|
| + base::mac::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_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 +177,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 +188,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,70 +199,37 @@
|
| match_dict,
|
| &iterator_ref);
|
| if (kr != KERN_SUCCESS) {
|
| - LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name
|
| - << ": kernel error " << kr;
|
| + LOG(ERROR) << "IOServiceGetMatchingServices: " << kr;
|
| return false;
|
| }
|
| ScopedIOObject<io_iterator_t> iterator(iterator_ref);
|
| 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: 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: 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 +343,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 +357,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::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() {
|
| @@ -296,7 +391,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 +449,217 @@
|
| 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),
|
| + run_loop_running(false) {
|
| + }
|
| +
|
| + base::mac::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;
|
| + 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::mac::ScopedCFTypeRef<DASessionRef> session(DASessionCreate(NULL));
|
| + if (!session.get()) {
|
| + LOG(ERROR) << "DASessionCreate";
|
| + return;
|
| + }
|
| +
|
| + base::mac::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;
|
| + }
|
| +
|
| + 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) {
|
| + LOG(ERROR) << "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(ERROR) << "FNNotifyByPath: " << status;
|
| + return;
|
| + }
|
| +}
|
|
|