diff options
author | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-01-03 23:37:25 +0000 |
---|---|---|
committer | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-01-03 23:37:25 +0000 |
commit | 366986e3ba2b792ca701689747900480fa36fbba (patch) | |
tree | 1817e520c041f707bd87f5f3d18be28abb4aff36 | |
parent | 5baaeb28091dd6c386ab6535fe9f8c41793976e1 (diff) | |
download | chromium_src-366986e3ba2b792ca701689747900480fa36fbba.zip chromium_src-366986e3ba2b792ca701689747900480fa36fbba.tar.gz chromium_src-366986e3ba2b792ca701689747900480fa36fbba.tar.bz2 |
Expand the Keystone tag to contain the system's CPU's bitness and whether a
full installer is desired.
Formerly, the tag identified only the channel that Chrome was on. The tag is
being enhanced to detect the CPU's bitness (adding "-32bit" for 32-bit-only,
non-64-bit-capable CPUs) and whether a full (as opposed to binary diff patch)
update is requested (adding "-full").
CPU bitness detection ought to be a feature of Keystone, but Keystone uses the
NXGetLocalArchInfo to determine the CPU type, and winds up always reporting
"i486". The "-32bit" tag suffix will be present whenever the
"hw.cpu64bit_capable" sysctl name is not found or has value 0. This enables
proper detection of users who are capable of running 64-bit Chrome on the
server side.
When a binary diff patch update application fails in dirpatcher, typically the
result of modifications made to existing installations, the "-full" tag suffix
will be set. On a subsequent update attempt, the server can detect this value
and provide the client with a full updater package, which does not depend on
the existing installation. The "-full" tag suffix is cleared on successful
update installation.
BUG=18323,54047,225352,303280,316916
R=thakis@chromium.org
Review URL: https://codereview.chromium.org/102963007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@242964 0039d316-1c4b-4281-b951-d872f2087c98
-rwxr-xr-x | build/mac/tweak_info_plist.py | 28 | ||||
-rw-r--r-- | chrome/browser/mac/keystone_glue.mm | 83 | ||||
-rwxr-xr-x | chrome/installer/mac/keystone_install.sh | 162 |
3 files changed, 261 insertions, 12 deletions
diff --git a/build/mac/tweak_info_plist.py b/build/mac/tweak_info_plist.py index eb38ace..0f65e4a 100755 --- a/build/mac/tweak_info_plist.py +++ b/build/mac/tweak_info_plist.py @@ -204,6 +204,24 @@ def _RemoveBreakpadKeys(plist): 'BreakpadSkipConfirm') +def _TagSuffixes(): + # Keep this list sorted in the order that tag suffix components are to + # appear in a tag value. That is to say, it should be sorted per ASCII. + components = ('32bit', 'full') + assert tuple(sorted(components)) == components + + components_len = len(components) + combinations = 1 << components_len + tag_suffixes = [] + for combination in xrange(0, combinations): + tag_suffix = '' + for component_index in xrange(0, components_len): + if combination & (1 << component_index): + tag_suffix += '-' + components[component_index] + tag_suffixes.append(tag_suffix) + return tag_suffixes + + def _AddKeystoneKeys(plist, bundle_identifier): """Adds the Keystone keys. This must be called AFTER _AddVersionKeys() and also requires the |bundle_identifier| argument (com.example.product).""" @@ -211,6 +229,11 @@ def _AddKeystoneKeys(plist, bundle_identifier): plist['KSProductID'] = bundle_identifier plist['KSUpdateURL'] = 'https://tools.google.com/service/update2' + _RemoveKeys(plist, 'KSChannelID') + for tag_suffix in _TagSuffixes(): + if tag_suffix: + plist['KSChannelID' + tag_suffix] = tag_suffix + def _RemoveKeystoneKeys(plist): """Removes any set Keystone keys.""" @@ -219,6 +242,11 @@ def _RemoveKeystoneKeys(plist): 'KSProductID', 'KSUpdateURL') + tag_keys = [] + for tag_suffix in _TagSuffixes(): + tag_keys.append('KSChannelID' + tag_suffix) + _RemoveKeys(plist, *tag_keys) + def Main(argv): parser = optparse.OptionParser('%prog [options]') diff --git a/chrome/browser/mac/keystone_glue.mm b/chrome/browser/mac/keystone_glue.mm index 3a4d59e..dea4bb4 100644 --- a/chrome/browser/mac/keystone_glue.mm +++ b/chrome/browser/mac/keystone_glue.mm @@ -7,6 +7,8 @@ #include <sys/mount.h> #include <sys/param.h> #include <sys/stat.h> +#include <sys/sysctl.h> +#include <sys/types.h> #include <vector> @@ -22,6 +24,7 @@ #include "base/memory/ref_counted.h" #include "base/strings/sys_string_conversions.h" #include "base/threading/worker_pool.h" +#include "build/build_config.h" #import "chrome/browser/mac/keystone_registration.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/chrome_version_info.h" @@ -188,6 +191,22 @@ class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> { // Returns the brand file path to use for Keystone. - (NSString*)brandFilePath; +// YES if the system's CPU is 32-bit-only, NO if it's 64-bit-capable. +- (BOOL)has32BitOnlyCPU; + +// YES if no update installation has succeeded since a binary diff patch +// installation failed. This signals the need to attempt a full installer +// which does not depend on applying a patch to existing files. +- (BOOL)wantsFullInstaller; + +// Returns an NSString* suitable for appending to a Chrome Keystone tag value +// or tag key. If the system has a 32-bit-only CPU, the tag suffix will +// contain the string "-32bit". If a full installer (as opposed to a binary +// diff/delta patch) is required, the tag suffix will contain the string +// "-full". If no special treatment is required, the tag suffix will be an +// empty string. +- (NSString*)tagSuffix; + @end // @interface KeystoneGlue (Private) NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification"; @@ -453,6 +472,13 @@ NSString* const kVersionKey = @"KSVersion"; brandKey = @""; } + // Note that channel_ is permitted to be an empty string, but it must not be + // nil. + DCHECK(channel_); + NSString* tagSuffix = [self tagSuffix]; + NSString* tagValue = [channel_ stringByAppendingString:tagSuffix]; + NSString* tagKey = [kChannelKey stringByAppendingString:tagSuffix]; + return [NSDictionary dictionaryWithObjectsAndKeys: version_, ksr::KSRegistrationVersionKey, appInfoPlistPath, ksr::KSRegistrationVersionPathKey, @@ -461,9 +487,9 @@ NSString* const kVersionKey = @"KSVersion"; appPath_, ksr::KSRegistrationExistenceCheckerStringKey, url_, ksr::KSRegistrationServerURLStringKey, preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey, - channel_, ksr::KSRegistrationTagKey, + tagValue, ksr::KSRegistrationTagKey, appInfoPlistPath, ksr::KSRegistrationTagPathKey, - kChannelKey, ksr::KSRegistrationTagKeyKey, + tagKey, ksr::KSRegistrationTagKeyKey, brandPath, ksr::KSRegistrationBrandPathKey, brandKey, ksr::KSRegistrationBrandKeyKey, nil]; @@ -983,6 +1009,59 @@ NSString* const kVersionKey = @"KSVersion"; } } +- (BOOL)has32BitOnlyCPU { +#if defined(ARCH_CPU_64_BITS) + return NO; +#else // ARCH_CPU_64_BITS + int value; + size_t valueSize = sizeof(value); + if (sysctlbyname("hw.cpu64bit_capable", &value, &valueSize, NULL, 0) != 0) { + return YES; + } + return value == 0; +#endif // ARCH_CPU_64_BITS +} + +- (BOOL)wantsFullInstaller { + // It's difficult to check the tag prior to Keystone registration, and + // performing registration replaces the tag. keystone_install.sh + // communicates a need for a full installer with Chrome in this file, + // .want_full_installer. + NSString* wantFullInstallerPath = + [appPath_ stringByAppendingPathComponent:@".want_full_installer"]; + NSString* wantFullInstallerContents = + [NSString stringWithContentsOfFile:wantFullInstallerPath + encoding:NSUTF8StringEncoding + error:NULL]; + if (!wantFullInstallerContents) { + return NO; + } + + NSString* wantFullInstallerVersion = + [wantFullInstallerContents stringByTrimmingCharactersInSet: + [NSCharacterSet newlineCharacterSet]]; + return [wantFullInstallerVersion isEqualToString:version_]; +} + +- (NSString*)tagSuffix { + // Tag suffix components are not entirely arbitrary: all possible tag keys + // must be present in the application's Info.plist, there must be + // server-side agreement on the processing and meaning of tag suffix + // components, and other code that manipulates tag values (such as the + // Keystone update installation script) must be tag suffix-aware. To reduce + // the number of tag suffix combinations that need to be listed in + // Info.plist, tag suffix components should only be appended to the tag + // suffix in ASCII sort order. + NSString* tagSuffix = @""; + if ([self has32BitOnlyCPU]) { + tagSuffix = [tagSuffix stringByAppendingString:@"-32bit"]; + } + if ([self wantsFullInstaller]) { + tagSuffix = [tagSuffix stringByAppendingString:@"-full"]; + } + return tagSuffix; +} + @end // @implementation KeystoneGlue namespace { diff --git a/chrome/installer/mac/keystone_install.sh b/chrome/installer/mac/keystone_install.sh index 1dbb6ed..401e65f 100755 --- a/chrome/installer/mac/keystone_install.sh +++ b/chrome/installer/mac/keystone_install.sh @@ -29,13 +29,10 @@ # 9 Could not get the version, update URL, or channel after update # 10 Updated application does not have the version number from the update # 11 ksadmin failure -# 12 dirpatcher failed for versioned directory -# 13 dirpatcher failed for outer .app bundle # -# The following exit codes are not used by this script, but can be used to -# convey special meaning to Keystone: +# The following exit codes can be used to convey special meaning to Keystone: # 66 (unused) success, request reboot -# 77 (unused) try installation again later +# 77 try installation again later set -eu @@ -61,6 +58,8 @@ shopt -s nullglob ME="$(basename "${0}")" readonly ME +readonly KS_CHANNEL_KEY="KSChannelID" + # Workaround for http://code.google.com/p/chromium/issues/detail?id=83180#c3 # In bash 4.0, "declare VAR" no longer initializes VAR if not already set. : ${GOOGLE_CHROME_UPDATER_DEBUG:=} @@ -451,6 +450,14 @@ ksadmin_supports_versionpath_versionkey() { # return value. } +has_32_bit_only_cpu() { + local cpu_64_bit_capable="$(sysctl -n hw.cpu64bit_capable 2>/dev/null)" + [[ -z "${cpu_64_bit_capable}" || "${cpu_64_bit_capable}" -eq 0 ]] + + # The return value of the comparison is used as this function's return + # value. +} + # Runs "defaults read" to obtain the value of a key in a property list. As # with "defaults read", an absolute path to a plist is supplied, without the # ".plist" extension. @@ -484,6 +491,115 @@ infoplist_read() { __CFPREFERENCES_AVOID_DAEMON=1 defaults read "${@}" } +# When a patch update fails because the old installed copy doesn't match the +# expected state, mark_failed_patch_update updates the Keystone ticket by +# adding "-full" to the tag. The server will see this on a subsequent update +# attempt and will provide a full update (as opposed to a patch) to the +# client. +# +# Even if mark_failed_patch_update fails to modify the tag, the user will +# eventually be updated. Patch updates are only provided for successive +# releases on a particular channel, to update version o to version o+1. If a +# patch update fails in this case, eventually version o+2 will be released, +# and no patch update will exist to update o to o+2, so the server will +# provide a full update package. +mark_failed_patch_update() { + local product_id="${1}" + local want_full_installer_path="${2}" + local old_ks_plist="${3}" + local old_version_app="${4}" + local system_ticket="${5}" + + set +e + + note "marking failed patch update" + + local channel + channel="$(infoplist_read "${old_ks_plist}" "${KS_CHANNEL_KEY}" 2> /dev/null)" + + local tag="${channel}" + local tag_key="${KS_CHANNEL_KEY}" + if has_32_bit_only_cpu; then + tag="${tag}-32bit" + tag_key="${tag_key}-32bit" + fi + + tag="${tag}-full" + tag_key="${tag_key}-full" + + note "tag = ${tag}" + note "tag_key = ${tag_key}" + + # ${old_ks_plist}, used for --tag-path, is the Info.plist for the old + # version of Chrome. It may not contain the keys for the "-full" tag suffix. + # If it doesn't, just bail out without marking the patch update as failed. + local read_tag="$(infoplist_read "${old_ks_plist}" "${tag_key}" 2> /dev/null)" + note "read_tag = ${read_tag}" + if [[ -z "${read_tag}" ]]; then + note "couldn't mark failed patch update" + return 0 + fi + + # Chrome can't easily read its Keystone ticket prior to registration, and + # when Chrome registers with Keystone, it obliterates old tag values in its + # ticket. Therefore, an alternative mechanism is provided to signal to + # Chrome that a full installer is desired. If the .want_full_installer file + # is present and it contains Chrome's current version number, Chrome will + # include "-full" in its tag when it registers with Keystone. This allows + # "-full" to persist in the tag even after Chrome is relaunched, which on a + # user ticket, triggers a re-registration. + # + # .want_full_installer is placed immediately inside the .app bundle as a + # sibling to the Contents directory. In this location, it's outside of the + # view of the code signing and code signature verification machinery. This + # file can safely be added, modified, and removed without affecting the + # signature. + rm -f "${want_full_installer_path}" 2> /dev/null + echo "${old_version_app}" > "${want_full_installer_path}" + + # See the comment below in the "setting permissions" section for an + # explanation of the groups and modes selected here. + local chmod_mode="644" + if [[ -z "${system_ticket}" ]] && + [[ "${want_full_installer_path:0:14}" = "/Applications/" ]] && + chgrp admin "${want_full_installer_path}" 2> /dev/null; then + chmod_mode="664" + fi + note "chmod_mode = ${chmod_mode}" + chmod "${chmod_mode}" "${want_full_installer_path}" 2> /dev/null + + local old_ks_plist_path="${old_ks_plist}.plist" + + # Using ksadmin without --register only updates specified values in the + # ticket, without changing other existing values. + local ksadmin_args=( + --productid "${product_id}" + ) + + if ksadmin_supports_tag; then + ksadmin_args+=( + --tag "${tag}" + ) + fi + + if ksadmin_supports_tagpath_tagkey; then + ksadmin_args+=( + --tag-path "${old_ks_plist_path}" + --tag-key "${tag_key}" + ) + fi + + note "ksadmin_args = ${ksadmin_args[*]}" + + if ! ksadmin "${ksadmin_args[@]}"; then + err "ksadmin failed" + fi + + note "marked failed patch update" + + set -e +} + usage() { echo "usage: ${ME} update_dmg_mount_point" >& 2 } @@ -513,7 +629,6 @@ main() { readonly KS_VERSION_KEY="KSVersion" readonly KS_PRODUCT_KEY="KSProductID" readonly KS_URL_KEY="KSUpdateURL" - readonly KS_CHANNEL_KEY="KSChannelID" readonly KS_BRAND_KEY="KSBrandID" readonly QUARANTINE_ATTR="com.apple.quarantine" @@ -740,6 +855,9 @@ main() { fi note "installed_app = ${installed_app}" + local want_full_installer_path="${installed_app}/.want_full_installer" + note "want_full_installer_path = ${want_full_installer_path}" + if [[ "${installed_app:0:1}" != "/" ]] || ! [[ -d "${installed_app}" ]]; then err "installed_app must be an absolute path to a directory" @@ -902,7 +1020,12 @@ main() { "${patch_versioned_dir}" \ "${versioned_dir_target}"; then err "dirpatcher of versioned directory failed, status ${PIPESTATUS[0]}" - exit 12 + mark_failed_patch_update "${product_id}" \ + "${want_full_installer_path}" \ + "${old_ks_plist}" \ + "${old_version_app}" \ + "${system_ticket}" + exit 77 fi fi @@ -956,7 +1079,12 @@ main() { "${patch_app_dir}" \ "${update_app}"; then err "dirpatcher of app directory failed, status ${PIPESTATUS[0]}" - exit 13 + mark_failed_patch_update "${product_id}" \ + "${want_full_installer_path}" \ + "${old_ks_plist}" \ + "${old_version_app}" \ + "${system_ticket}" + exit 77 fi fi @@ -1004,6 +1132,11 @@ main() { note "g_temp_dir = ${g_temp_dir}" fi + # Clean up any old .want_full_installer files from previous dirpatcher + # failures. This is not considered a critical step, because this file + # normally does not exist at all. + rm -f "${want_full_installer_path}" || true + # If necessary, touch the outermost .app so that it appears to the outside # world that something was done to the bundle. This will cause # LaunchServices to invalidate the information it has cached about the @@ -1057,6 +1190,15 @@ main() { "${KS_CHANNEL_KEY}" 2> /dev/null || true)" note "channel = ${channel}" + local tag="${channel}" + local tag_key="${KS_CHANNEL_KEY}" + if has_32_bit_only_cpu; then + tag="${tag}-32bit" + tag_key="${tag_key}-32bit" + fi + note "tag = ${tag}" + note "tag_key = ${tag_key}" + # Make sure that the update was successful by comparing the version found in # the update with the version now on disk. if [[ "${new_version_ks}" != "${update_version_ks}" ]]; then @@ -1165,14 +1307,14 @@ main() { if ksadmin_supports_tag; then ksadmin_args+=( - --tag "${channel}" + --tag "${tag}" ) fi if ksadmin_supports_tagpath_tagkey; then ksadmin_args+=( --tag-path "${installed_app_plist_path}" - --tag-key "${KS_CHANNEL_KEY}" + --tag-key "${tag_key}" ) fi |