diff options
author | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-02 21:26:58 +0000 |
---|---|---|
committer | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-02 21:26:58 +0000 |
commit | 0a357e63d5bc32ef031cc7cf9b741db9b023958b (patch) | |
tree | 438ee2c1328d6c5264887a349be9f1171cfaa105 /chrome | |
parent | dba272a4d92316f0ef35065aa8a1eb94fa950430 (diff) | |
download | chromium_src-0a357e63d5bc32ef031cc7cf9b741db9b023958b.zip chromium_src-0a357e63d5bc32ef031cc7cf9b741db9b023958b.tar.gz chromium_src-0a357e63d5bc32ef031cc7cf9b741db9b023958b.tar.bz2 |
In-app installation when running from a disk image.
BUG=28986
TEST=Launch from a disk image without having a copy already installed in
/Applications, and with permission to write to /Applications.
Review URL: http://codereview.chromium.org/1599011
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@43527 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r-- | chrome/app/generated_resources.grd | 46 | ||||
-rw-r--r-- | chrome/browser/browser_main.cc | 16 | ||||
-rw-r--r-- | chrome/browser/browser_main_mac.mm | 1 | ||||
-rw-r--r-- | chrome/browser/cocoa/install_from_dmg.h | 14 | ||||
-rw-r--r-- | chrome/browser/cocoa/install_from_dmg.mm | 432 | ||||
-rwxr-xr-x | chrome/chrome_browser.gypi | 2 |
6 files changed, 511 insertions, 0 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 050d5a7..0b903a1 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -7127,6 +7127,52 @@ Keep your key file in a safe place. You will need it to create new versions of y <message name="IDS_SAVE_PAGE_FILE_FORMAT_PROMPT_MAC" desc="The title of the File Format label for saving a page."> Format: </message> + + <!-- Install from disk image --> + <message name="IDS_INSTALL_FROM_DMG_TITLE" desc="Title of the dialog asking whether to install from the disk image. Mac-only."> + Do you want to install <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph>? + </message> + <message name="IDS_INSTALL_FROM_DMG_PROMPT" desc="Prompt asking whether to install from the disk image. Mac-only."> + You’re running <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> from its disk image. Installing it on your computer lets you run it without the disk image, and ensures it will be kept up to date. + </message> + <message name="IDS_INSTALL_FROM_DMG_YES" desc="Button to approve installation from the disk image. Mac-only."> + Install + </message> + <message name="IDS_INSTALL_FROM_DMG_NO" desc="Button to cancel installation from the disk image. Mac-only."> + Don’t Install + </message> + <message name="IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT" desc="The prompt to be displayed in the authentication dialog when installing from the disk image. The system will add a sentence asking for an administrator's name and password. Mac-only."> + <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> will be installed. + </message> + <message name="IDS_INSTALL_FROM_DMG_ERROR_TITLE" desc="Error dialog title to be displayed when installation from the disk image fails. Mac-only."> + Installation failed. + </message> + <message name="IDS_INSTALL_FROM_DMG_ERROR" desc="Error dialog message to be displayed when installation from the disk image fails. Mac-only."> + <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> couldn’t complete installation, but will continue to run from its disk image. + </message> + + <!-- Update from disk image --> + <message name="IDS_UPDATE_FROM_DMG_TITLE" desc="Title of the dialog asking whether to update from the disk image. Mac-only."> + Do you want to update <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph>? + </message> + <message name="IDS_UPDATE_FROM_DMG_PROMPT" desc="Prompt asking whether to update from the disk image. Mac-only."> + You’re running <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> from its disk image. If you update the installed copy, you can run it without the disk image in the future. + </message> + <message name="IDS_UPDATE_FROM_DMG_YES" desc="Button to approve update from the disk image. Mac-only."> + Update + </message> + <message name="IDS_UPDATE_FROM_DMG_NO" desc="Button to cancel update from the disk image. Mac-only."> + Don’t Update + </message> + <message name="IDS_UPDATE_FROM_DMG_AUTHENTICATION_PROMPT" desc="The prompt to be displayed in the authentication dialog when updating from the disk image. The system will add a sentence asking for an administrator's name and password. Mac-only."> + <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> will be updated. + </message> + <message name="IDS_UPDATE_FROM_DMG_ERROR_TITLE" desc="Error dialog title to be displayed when update from the disk image fails. Mac-only."> + Update failed. + </message> + <message name="IDS_UPDATE_FROM_DMG_ERROR" desc="Error dialog message to be displayed when update from the disk image fails. Mac-only."> + <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> couldn’t update the installed copy, but will continue to run from its disk image. + </message> </if> <!-- Filebrowser Strings --> diff --git a/chrome/browser/browser_main.cc b/chrome/browser/browser_main.cc index fe83d99..20b0a07 100644 --- a/chrome/browser/browser_main.cc +++ b/chrome/browser/browser_main.cc @@ -127,6 +127,10 @@ #include "sandbox/src/sandbox.h" #endif // defined(OS_WIN) +#if defined(OS_MACOSX) +#include "chrome/browser/cocoa/install_from_dmg.h" +#endif + #if defined(TOOLKIT_VIEWS) #include "chrome/browser/views/chrome_views_delegate.h" #include "views/focus/accelerator_handler.h" @@ -973,6 +977,18 @@ int BrowserMain(const MainFunctionParams& parameters) { // Create the TranslateManager singleton. Singleton<TranslateManager>::get(); +#if defined(OS_MACOSX) + if (!parsed_command_line.HasSwitch(switches::kNoFirstRun)) { + // Disk image installation is sort of a first-run task, so it shares the + // kNoFirstRun switch. + if (MaybeInstallFromDiskImage()) { + // The application was installed and the installed copy has been + // launched. This process is now obsolete. Exit. + return ResultCodes::NORMAL_EXIT; + } + } +#endif + // Show the First Run UI if this is the first time Chrome has been run on // this computer, or we're being compelled to do so by a command line flag. // Note that this be done _after_ the PrefService is initialized and all diff --git a/chrome/browser/browser_main_mac.mm b/chrome/browser/browser_main_mac.mm index 38d3395..266ae24 100644 --- a/chrome/browser/browser_main_mac.mm +++ b/chrome/browser/browser_main_mac.mm @@ -43,6 +43,7 @@ void WillInitializeMainMessageLoop(const MainFunctionParams& parameters) { // have the strings avaiable for localization. ResourceBundle::InitSharedInstance(std::wstring()); } + // Now load the nib. [NSBundle loadNibNamed:@"MainMenu" owner:NSApp]; diff --git a/chrome/browser/cocoa/install_from_dmg.h b/chrome/browser/cocoa/install_from_dmg.h new file mode 100644 index 0000000..437973d --- /dev/null +++ b/chrome/browser/cocoa/install_from_dmg.h @@ -0,0 +1,14 @@ +// 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. + +#ifndef CHROME_BROWSER_COCOA_INSTALL_FROM_DMG_H_ +#define CHROME_BROWSER_COCOA_INSTALL_FROM_DMG_H_ + +// If the application is running from a read-only disk image, prompts the user +// to install it to the hard drive. If the user approves, the application +// will be installed and launched, and MaybeInstallFromDiskImage will return +// true. In that case, the caller must exit expeditiously. +bool MaybeInstallFromDiskImage(); + +#endif // CHROME_BROWSER_COCOA_INSTALL_FROM_DMG_H_ diff --git a/chrome/browser/cocoa/install_from_dmg.mm b/chrome/browser/cocoa/install_from_dmg.mm new file mode 100644 index 0000000..d6e28f0 --- /dev/null +++ b/chrome/browser/cocoa/install_from_dmg.mm @@ -0,0 +1,432 @@ +// 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; +} diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 383018d..0d88f93 100755 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -694,6 +694,8 @@ 'browser/cocoa/infobar_gradient_view.h', 'browser/cocoa/infobar_gradient_view.mm', 'browser/cocoa/infobar_test_helper.h', + 'browser/cocoa/install_from_dmg.h', + 'browser/cocoa/install_from_dmg.mm', 'browser/cocoa/keystone_glue.h', 'browser/cocoa/keystone_glue.mm', 'browser/cocoa/keystone_infobar.h', |