// Copyright (c) 2010 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/cocoa/install_from_dmg.h" #include <ApplicationServices/ApplicationServices.h> #import <AppKit/AppKit.h> #include <CoreFoundation/CoreFoundation.h> #include <CoreServices/CoreServices.h> #include <IOKit/IOKitLib.h> #include <string.h> #include <sys/param.h> #include <sys/mount.h> #import "app/l10n_util_mac.h" #include "base/basictypes.h" #include "base/command_line.h" #include "base/logging.h" #include "base/scoped_nsautorelease_pool.h" #include "base/sys_info.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.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 { // Just like scoped_cftyperef from base/scoped_cftyperef.h, but for // io_object_t and subclasses. template<typename IOT> class scoped_ioobject { public: typedef IOT element_type; explicit scoped_ioobject(IOT object = NULL) : object_(object) { } ~scoped_ioobject() { if (object_) IOObjectRelease(object_); } void reset(IOT object = NULL) { if (object_) IOObjectRelease(object_); object_ = object; } bool operator==(IOT that) const { return object_ == that; } bool operator!=(IOT that) const { return object_ != that; } operator IOT() const { return object_; } IOT get() const { return object_; } void swap(scoped_ioobject& that) { IOT temp = that.object_; that.object_ = object_; object_ = temp; } IOT release() { IOT temp = object_; object_ = NULL; return temp; } private: IOT object_; DISALLOW_COPY_AND_ASSIGN(scoped_ioobject); }; // 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[]) { 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* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length; 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, bsd_device_name); if (!match_dict) { LOG(ERROR) << "IOBSDNameMatching " << 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) { LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name << ": kernel error " << kr; return false; } scoped_ioobject<io_iterator_t> iterator(iterator_ref); iterator_ref = NULL; // There needs to be exactly one matching service. scoped_ioobject<io_service_t> filesystem_service(IOIteratorNext(iterator)); if (!filesystem_service) { LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service"; return false; } scoped_ioobject<io_service_t> unexpected_service(IOIteratorNext(iterator)); if (unexpected_service) { LOG(ERROR) << "IOIteratorNext " << 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(scoped_ioobject<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; } // 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() { return IsPathOnReadOnlyDiskImage( [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]); } // 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; } // Copies source_path to target_path and performs any additional on-disk // bookkeeping needed to be able to launch target_path properly. bool InstallFromDiskImage(NSString* source_path, NSString* target_path) { NSFileManager* file_manager = [NSFileManager defaultManager]; // For the purposes of this copy, the file manager's delegate shouldn't be // consulted at all. Clear the delegate and restore it after the copy is // done. id file_manager_delegate = [file_manager delegate]; [file_manager setDelegate:nil]; NSError* copy_error; bool copy_result = [file_manager copyItemAtPath:source_path toPath:target_path error:©_error]; [file_manager setDelegate:file_manager_delegate]; if (!copy_result) { LOG(ERROR) << "-[NSFileManager copyItemAtPath:toPath:error:]: " << [[copy_error description] UTF8String]; return false; } // Since the application performed the copy, and the application has the // quarantine bit (LSFileQuarantineEnabled) set, the installed copy will // be quarantined. That's bad, because it will cause the quarantine dialog // to be displayed, possibly after a long delay, when the application is // relaunched. Use xattr to drop the quarantine attribute. // // There are three reasons not to use MDItemRemoveAttribute directly: // 1. MDItemRemoveAttribute is a private API. // 2. The operation needs to be recursive, and writing a bunch of code to // handle the recursion just to call a private API is annoying. // 3. All of this stuff will likely move into a shell script anyway, and // the shell script will have no choice but to use xattr. int32 os_major, os_minor, os_patch; base::SysInfo::OperatingSystemVersionNumbers(&os_major, &os_minor, &os_patch); const NSString* xattr_path = @"/usr/bin/xattr"; const NSString* quarantine_attribute = @"com.apple.quarantine"; NSString* launch_path; NSArray* arguments; if (os_major > 10 || (os_major == 10 && os_minor >= 6)) { // On 10.6, xattr supports -r for recursive operation. launch_path = xattr_path; arguments = [NSArray arrayWithObjects:@"-r", @"-d", quarantine_attribute, target_path, nil]; } else { // On earlier systems, xattr doesn't support -r, so run xattr via find. launch_path = @"/usr/bin/find"; arguments = [NSArray arrayWithObjects:target_path, @"-exec", xattr_path, @"-d", quarantine_attribute, @"{}", @"+", nil]; } NSTask* task; @try { task = [NSTask launchedTaskWithLaunchPath:launch_path arguments:arguments]; } @catch(NSException* exception) { LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: " << [[exception description] UTF8String]; return false; } [task waitUntilExit]; int status = [task terminationStatus]; if (status != 0) { LOG(ERROR) << "/usr/bin/xattr: status " << status; return false; } return true; } // Launches the application at app_path. The arguments passed to app_path // will be the same as the arguments used to invoke this process, except any // arguments beginning with -psn_ will be stripped. bool LaunchInstalledApp(NSString* app_path) { const UInt8* app_path_c = reinterpret_cast<const UInt8*>([app_path fileSystemRepresentation]); FSRef app_fsref; OSStatus err = FSPathMakeRef(app_path_c, &app_fsref, NULL); if (err != noErr) { LOG(ERROR) << "FSPathMakeRef: " << err; return false; } // Use an empty dictionary for the environment. NSDictionary* environment = [NSDictionary dictionary]; const std::vector<std::string>& argv = CommandLine::ForCurrentProcess()->argv(); NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argv.size() - 1]; // Start at argv[1]. LSOpenApplication adds its own argv[0] as the path of // the launched executable. for (size_t index = 1; index < argv.size(); ++index) { std::string argument = argv[index]; const char psn_flag[] = "-psn_"; const int psn_flag_length = arraysize(psn_flag) - 1; if (argument.compare(0, psn_flag_length, psn_flag) != 0) { // Strip any -psn_ arguments, as they apply to a specific process. [arguments addObject:[NSString stringWithUTF8String:argument.c_str()]]; } } struct LSApplicationParameters parameters = {0}; parameters.flags = kLSLaunchDefaults; parameters.application = &app_fsref; parameters.environment = reinterpret_cast<CFDictionaryRef>(environment); parameters.argv = reinterpret_cast<CFArrayRef>(arguments); err = LSOpenApplication(¶meters, NULL); if (err != noErr) { LOG(ERROR) << "LSOpenApplication: " << err; return false; } return true; } 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::ScopedNSAutoreleasePool autorelease_pool; if (!IsAppRunningFromReadOnlyDiskImage()) { 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) { LOG(INFO) << "No application directory at " << [application_directory UTF8String]; return false; } // TODO(mark): When this happens, prompt for authentication. if (![file_manager isWritableFileAtPath:application_directory]) { LOG(INFO) << "Non-writable application directory at " << [application_directory UTF8String]; return false; } NSString* source_path = [[NSBundle mainBundle] bundlePath]; NSString* application_name = [source_path lastPathComponent]; NSString* target_path = [application_directory stringByAppendingPathComponent:application_name]; if ([file_manager fileExistsAtPath:target_path]) { LOG(INFO) << "Something already exists at " << [target_path UTF8String]; return false; } if (!ShouldInstallDialog()) { return false; } if (!InstallFromDiskImage(source_path, target_path) || !LaunchInstalledApp(target_path)) { ShowErrorDialog(); return false; } return true; }