Index: chrome_mac/Google Chrome Packaging/keystone_install.sh |
=================================================================== |
--- chrome_mac/Google Chrome Packaging/keystone_install.sh (revision 0) |
+++ chrome_mac/Google Chrome Packaging/keystone_install.sh (revision 0) |
@@ -0,0 +1,1414 @@ |
+#!/bin/bash -p |
+ |
+# Copyright (c) 2012 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. |
+ |
+# usage: keystone_install.sh update_dmg_mount_point |
+# |
+# Called by the Keystone system to update the installed application with a new |
+# version from a disk image. |
+# |
+# Environment variables: |
+# GOOGLE_CHROME_UPDATER_DEBUG |
+# When set to a non-empty value, additional information about this script's |
+# actions will be logged to stderr. The same debugging information will |
+# also be enabled when "Library/Google/Google Chrome Updater Debug" in the |
+# root directory or in ${HOME} exists. |
+# |
+# Exit codes: |
+# 0 Happiness |
+# 1 Unknown failure |
+# 2 Basic sanity check source failure (e.g. no app on disk image) |
+# 3 Basic sanity check destination failure (e.g. ticket points to nothing) |
+# 4 Update driven by user ticket when a system ticket is also present |
+# 5 Could not prepare existing installed version to receive update |
+# 6 Patch sanity check failure |
+# 7 rsync failed (could not copy new versioned directory to Versions) |
+# 8 rsync failed (could not update outer .app bundle) |
+# 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: |
+# 66 (unused) success, request reboot |
+# 77 (unused) try installation again later |
+ |
+set -eu |
+ |
+# http://b/2290916: Keystone runs the installation with a restrictive PATH |
+# that only includes the directory containing ksadmin, /bin, and /usr/bin. It |
+# does not include /sbin or /usr/sbin. This script uses lsof, which is in |
+# /usr/sbin, and it's conceivable that it might want to use other tools in an |
+# sbin directory. Adjust the path accordingly. |
+export PATH="${PATH}:/sbin:/usr/sbin" |
+ |
+# Environment sanitization. Clear environment variables that might impact the |
+# interpreter's operation. The |bash -p| invocation on the #! line takes the |
+# bite out of BASH_ENV, ENV, and SHELLOPTS (among other features), but |
+# clearing them here ensures that they won't impact any shell scripts used as |
+# utility programs. SHELLOPTS is read-only and can't be unset, only |
+# unexported. |
+unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT |
+export -n SHELLOPTS |
+ |
+set -o pipefail |
+shopt -s nullglob |
+ |
+ME="$(basename "${0}")" |
+readonly ME |
+ |
+# 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:=} |
+err() { |
+ local error="${1}" |
+ |
+ local id= |
+ if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then |
+ id=": ${$} $(date "+%Y-%m-%d %H:%M:%S %z")" |
+ fi |
+ |
+ echo "${ME}${id}: ${error}" >& 2 |
+} |
+ |
+note() { |
+ local message="${1}" |
+ |
+ if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then |
+ err "${message}" |
+ fi |
+} |
+ |
+g_temp_dir= |
+cleanup() { |
+ local status=${?} |
+ |
+ trap - EXIT |
+ trap '' HUP INT QUIT TERM |
+ |
+ if [[ ${status} -ge 128 ]]; then |
+ err "Caught signal $((${status} - 128))" |
+ fi |
+ |
+ if [[ -n "${g_temp_dir}" ]]; then |
+ rm -rf "${g_temp_dir}" |
+ fi |
+ |
+ exit ${status} |
+} |
+ |
+ensure_temp_dir() { |
+ if [[ -z "${g_temp_dir}" ]]; then |
+ # Choose a template that won't be a dot directory. Make it safe by |
+ # removing leading hyphens, too. |
+ local template="${ME}" |
+ if [[ "${template}" =~ ^[-.]+(.*)$ ]]; then |
+ template="${BASH_REMATCH[1]}" |
+ fi |
+ if [[ -z "${template}" ]]; then |
+ template="keystone_install" |
+ fi |
+ |
+ g_temp_dir="$(mktemp -d -t "${template}")" |
+ note "g_temp_dir = ${g_temp_dir}" |
+ fi |
+} |
+ |
+# Returns 0 (true) if |symlink| exists, is a symbolic link, and appears |
+# writable on the basis of its POSIX permissions. This is used to determine |
+# writability like test's -w primary, but -w resolves symbolic links and this |
+# function does not. |
+is_writable_symlink() { |
+ local symlink="${1}" |
+ |
+ local link_mode |
+ link_mode="$(stat -f %Sp "${symlink}" 2> /dev/null || true)" |
+ if [[ -z "${link_mode}" ]] || [[ "${link_mode:0:1}" != "l" ]]; then |
+ return 1 |
+ fi |
+ |
+ local link_user link_group |
+ link_user="$(stat -f %u "${symlink}" 2> /dev/null || true)" |
+ link_group="$(stat -f %g "${symlink}" 2> /dev/null || true)" |
+ if [[ -z "${link_user}" ]] || [[ -z "${link_group}" ]]; then |
+ return 1 |
+ fi |
+ |
+ # If the users match, check the owner-write bit. |
+ if [[ ${EUID} -eq "${link_user}" ]]; then |
+ if [[ "${link_mode:2:1}" = "w" ]]; then |
+ return 0 |
+ fi |
+ return 1 |
+ fi |
+ |
+ # If the file's group matches any of the groups that this process is a |
+ # member of, check the group-write bit. |
+ local group_match= |
+ local group |
+ for group in "${GROUPS[@]}"; do |
+ if [[ "${group}" -eq "${link_group}" ]]; then |
+ group_match="y" |
+ break |
+ fi |
+ done |
+ if [[ -n "${group_match}" ]]; then |
+ if [[ "${link_mode:5:1}" = "w" ]]; then |
+ return 0 |
+ fi |
+ return 1 |
+ fi |
+ |
+ # Check the other-write bit. |
+ if [[ "${link_mode:8:1}" = "w" ]]; then |
+ return 0 |
+ fi |
+ |
+ return 1 |
+} |
+ |
+# If |symlink| exists and is a symbolic link, but is not writable according to |
+# is_writable_symlink, this function attempts to replace it with a new |
+# writable symbolic link. If |symlink| does not exist, is not a symbolic |
+# link, or is already writable, this function does nothing. This function |
+# always returns 0 (true). |
+ensure_writable_symlink() { |
+ local symlink="${1}" |
+ |
+ if [[ -L "${symlink}" ]] && ! is_writable_symlink "${symlink}"; then |
+ # If ${symlink} refers to a directory, doing this naively might result in |
+ # the new link being placed in that directory, instead of replacing the |
+ # existing link. ln -fhs is supposed to handle this case, but it does so |
+ # by unlinking (removing) the existing symbolic link before creating a new |
+ # one. That leaves a small window during which the symbolic link is not |
+ # present on disk at all. |
+ # |
+ # To avoid that possibility, a new symbolic link is created in a temporary |
+ # location and then swapped into place with mv. An extra temporary |
+ # directory is used to convince mv to replace the symbolic link: again, if |
+ # the existing link refers to a directory, "mv newlink oldlink" will |
+ # actually leave oldlink alone and place newlink into the directory. |
+ # "mv newlink dirname(oldlink)" works as expected, but in order to replace |
+ # oldlink, newlink must have the same basename, hence the temporary |
+ # directory. |
+ |
+ local target |
+ target="$(readlink "${symlink}" 2> /dev/null || true)" |
+ if [[ -z "${target}" ]]; then |
+ return 0 |
+ fi |
+ |
+ # Error handling strategy: if anything fails, such as the mktemp, ln, |
+ # chmod, or mv, ignore the failure and return 0 (success), leaving the |
+ # existing state with the non-writable symbolic link intact. Failures |
+ # in this function will be difficult to understand and diagnose, and a |
+ # non-writable symbolic link is not necessarily fatal. If something else |
+ # requires a writable symbolic link, allowing it to fail when a symbolic |
+ # link is not writable is easier to understand than bailing out of the |
+ # script on failure here. |
+ |
+ local symlink_dir temp_link_dir temp_link |
+ symlink_dir="$(dirname "${symlink}")" |
+ temp_link_dir="$(mktemp -d "${symlink_dir}/.symlink_temp.XXXXXX" || true)" |
+ if [[ -z "${temp_link_dir}" ]]; then |
+ return 0 |
+ fi |
+ temp_link="${temp_link_dir}/$(basename "${symlink}")" |
+ |
+ (ln -fhs "${target}" "${temp_link}" && \ |
+ chmod -h 755 "${temp_link}" && \ |
+ mv -f "${temp_link}" "${symlink_dir}/") || true |
+ rm -rf "${temp_link_dir}" |
+ fi |
+ |
+ return 0 |
+} |
+ |
+# ensure_writable_symlinks_recursive calls ensure_writable_symlink for every |
+# symbolic link in |directory|, recursively. |
+# |
+# In some very weird and rare cases, it is possible to wind up with a user |
+# installation that contains symbolic links that the user does not have write |
+# permission over. More on how that might happen later. |
+# |
+# If a weird and rare case like this is observed, rsync will exit with an |
+# error when attempting to update the times on these symbolic links. rsync |
+# may not be intelligent enough to try creating a new symbolic link in these |
+# cases, but this script can be. |
+# |
+# The problem occurs when an administrative user first drag-installs the |
+# application to /Applications, resulting in the program's user being set to |
+# the user's own ID. If, subsequently, a .pkg package is installed over that, |
+# the existing directory ownership will be preserved, but file ownership will |
+# be changed to whatever is specified by the package, typically root. This |
+# applies to symbolic links as well. On a subsequent update, rsync will be |
+# able to copy the new files into place, because the user still has permission |
+# to write to the directories. If the symbolic link targets are not changing, |
+# though, rsync will not replace them, and they will remain owned by root. |
+# The user will not have permission to update the time on the symbolic links, |
+# resulting in an rsync error. |
+ensure_writable_symlinks_recursive() { |
+ local directory="${1}" |
+ |
+ # This fix-up is not necessary when running as root, because root will |
+ # always be able to write everything needed. |
+ if [[ ${EUID} -eq 0 ]]; then |
+ return 0 |
+ fi |
+ |
+ # This step isn't critical. |
+ local set_e= |
+ if [[ "${-}" =~ e ]]; then |
+ set_e="y" |
+ set +e |
+ fi |
+ |
+ # Use find -print0 with read -d $'\0' to handle even the weirdest paths. |
+ local symlink |
+ while IFS= read -r -d $'\0' symlink; do |
+ ensure_writable_symlink "${symlink}" |
+ done < <(find "${directory}" -type l -print0) |
+ |
+ # Go back to how things were. |
+ if [[ -n "${set_e}" ]]; then |
+ set -e |
+ fi |
+} |
+ |
+# is_version_ge accepts two version numbers, left and right, and performs a |
+# piecewise comparison determining the result of left >= right, returning true |
+# (0) if left >= right, and false (1) if left < right. If left or right are |
+# missing components relative to the other, the missing components are assumed |
+# to be 0, such that 10.6 == 10.6.0. |
+is_version_ge() { |
+ local left="${1}" |
+ local right="${2}" |
+ |
+ local -a left_array right_array |
+ IFS=. left_array=(${left}) |
+ IFS=. right_array=(${right}) |
+ |
+ local left_count=${#left_array[@]} |
+ local right_count=${#right_array[@]} |
+ local count=${left_count} |
+ if [[ ${right_count} -lt ${count} ]]; then |
+ count=${right_count} |
+ fi |
+ |
+ # Compare the components piecewise, as long as there are corresponding |
+ # components on each side. If left_element and right_element are unequal, |
+ # a comparison can be made. |
+ local index=0 |
+ while [[ ${index} -lt ${count} ]]; do |
+ local left_element="${left_array[${index}]}" |
+ local right_element="${right_array[${index}]}" |
+ if [[ ${left_element} -gt ${right_element} ]]; then |
+ return 0 |
+ elif [[ ${left_element} -lt ${right_element} ]]; then |
+ return 1 |
+ fi |
+ ((++index)) |
+ done |
+ |
+ # If there are more components on the left than on the right, continue |
+ # comparing, assuming 0 for each of the missing components on the right. |
+ while [[ ${index} -lt ${left_count} ]]; do |
+ local left_element="${left_array[${index}]}" |
+ if [[ ${left_element} -gt 0 ]]; then |
+ return 0 |
+ fi |
+ ((++index)) |
+ done |
+ |
+ # If there are more components on the right than on the left, continue |
+ # comparing, assuming 0 for each of the missing components on the left. |
+ while [[ ${index} -lt ${right_count} ]]; do |
+ local right_element="${right_array[${index}]}" |
+ if [[ ${right_element} -gt 0 ]]; then |
+ return 1 |
+ fi |
+ ((++index)) |
+ done |
+ |
+ # Upon reaching this point, the two version numbers are semantically equal. |
+ return 0 |
+} |
+ |
+# Prints the OS version, as reported by sw_vers -productVersion, to stdout. |
+# This function operates with "static" variables: it will only check the OS |
+# version once per script run. |
+g_checked_os_version= |
+g_os_version= |
+os_version() { |
+ if [[ -z "${g_checked_os_version}" ]]; then |
+ g_checked_os_version="y" |
+ g_os_version="$(sw_vers -productVersion)" |
+ note "g_os_version = ${g_os_version}" |
+ fi |
+ echo "${g_os_version}" |
+ return 0 |
+} |
+ |
+# Compares the running OS version against a supplied version number, |
+# |check_version|, and returns 0 (true) if the running OS version is greater |
+# than or equal to |check_version| according to a piece-wise comparison. |
+# Returns 1 (false) if the running OS version number cannot be determined or |
+# if |check_version| is greater than the running OS version. |check_version| |
+# should be a string of the form "major.minor" or "major.minor.micro". |
+is_os_version_ge() { |
+ local check_version="${1}" |
+ |
+ local os_version="$(os_version)" |
+ is_version_ge "${os_version}" "${check_version}" |
+ |
+ # The return value of is_version_ge is used as this function's return value. |
+} |
+ |
+# Returns 0 (true) if xattr supports -r for recursive operation. |
+os_xattr_supports_r() { |
+ # xattr -r is supported in Mac OS X 10.6. |
+ is_os_version_ge 10.6 |
+ |
+ # The return value of is_os_version_ge is used as this function's return |
+ # value. |
+} |
+ |
+# Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to |
+# stdout. This function operates with "static" variables: it will only check |
+# the ksadmin version once per script run. If ksadmin is old enough to not |
+# support --ksadmin-version, or another error occurs, this function prints an |
+# empty string. |
+g_checked_ksadmin_version= |
+g_ksadmin_version= |
+ksadmin_version() { |
+ if [[ -z "${g_checked_ksadmin_version}" ]]; then |
+ g_checked_ksadmin_version="y" |
+ g_ksadmin_version="$(ksadmin --ksadmin-version || true)" |
+ note "g_ksadmin_version = ${g_ksadmin_version}" |
+ fi |
+ echo "${g_ksadmin_version}" |
+ return 0 |
+} |
+ |
+# Compares the installed ksadmin version against a supplied version number, |
+# |check_version|, and returns 0 (true) if the installed Keystone version is |
+# greater than or equal to |check_version| according to a piece-wise |
+# comparison. Returns 1 (false) if the installed Keystone version number |
+# cannot be determined or if |check_version| is greater than the installed |
+# Keystone version. |check_version| should be a string of the form |
+# "major.minor.micro.build". |
+is_ksadmin_version_ge() { |
+ local check_version="${1}" |
+ |
+ local ksadmin_version="$(ksadmin_version)" |
+ is_version_ge "${ksadmin_version}" "${check_version}" |
+ |
+ # The return value of is_version_ge is used as this function's return value. |
+} |
+ |
+# Returns 0 (true) if ksadmin supports --tag. |
+ksadmin_supports_tag() { |
+ local ksadmin_version |
+ |
+ ksadmin_version="$(ksadmin_version)" |
+ if [[ -n "${ksadmin_version}" ]]; then |
+ # A ksadmin that recognizes --ksadmin-version and provides a version |
+ # number is new enough to recognize --tag. |
+ return 0 |
+ fi |
+ |
+ return 1 |
+} |
+ |
+# Returns 0 (true) if ksadmin supports --tag-path and --tag-key. |
+ksadmin_supports_tagpath_tagkey() { |
+ # --tag-path and --tag-key were introduced in Keystone 1.0.7.1306. |
+ is_ksadmin_version_ge 1.0.7.1306 |
+ |
+ # The return value of is_ksadmin_version_ge is used as this function's |
+ # return value. |
+} |
+ |
+# Returns 0 (true) if ksadmin supports --brand-path and --brand-key. |
+ksadmin_supports_brandpath_brandkey() { |
+ # --brand-path and --brand-key were introduced in Keystone 1.0.8.1620. |
+ is_ksadmin_version_ge 1.0.8.1620 |
+ |
+ # The return value of is_ksadmin_version_ge is used as this function's |
+ # return value. |
+} |
+ |
+# Returns 0 (true) if ksadmin supports --version-path and --version-key. |
+ksadmin_supports_versionpath_versionkey() { |
+ # --version-path and --version-key were introduced in Keystone 1.0.9.2318. |
+ is_ksadmin_version_ge 1.0.9.2318 |
+ |
+ # The return value of is_ksadmin_version_ge 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. |
+# |
+# As of Mac OS X 10.8, defaults (and NSUserDefaults and CFPreferences) |
+# normally communicates with cfprefsd to read and write plists. Changes to a |
+# plist file aren't necessarily reflected immediately via this API family when |
+# not made through this API family, because cfprefsd may return cached data |
+# from a former on-disk version of a plist file instead of reading the current |
+# version from disk. The old behavior can be restored by setting the |
+# __CFPREFERENCES_AVOID_DAEMON environment variable, although extreme care |
+# should be used because portions of the system that use this API family |
+# normally and thus use cfprefsd and its cache will become unsynchronized with |
+# the on-disk state. |
+# |
+# This function is provided to set __CFPREFERENCES_AVOID_DAEMON when calling |
+# "defaults read" and thus avoid cfprefsd and its on-disk cache, and is |
+# intended only to be used to read values from Info.plist files, which are not |
+# preferences. The use of "defaults" for this purpose has always been |
+# questionable, but there's no better option to interact with plists from |
+# shell scripts. Definitely don't use infoplist_read to read preference |
+# plists. |
+# |
+# This function exists because the update process delivers new copies of |
+# Info.plist files to the disk behind cfprefsd's back, and if cfprefsd becomes |
+# aware of the original version of the file for any reason (such as this |
+# script reading values from it via "defaults read"), the new version of the |
+# file will not be immediately effective or visible via cfprefsd after the |
+# update is applied. |
+infoplist_read() { |
+ __CFPREFERENCES_AVOID_DAEMON=1 defaults read "${@}" |
+} |
+ |
+usage() { |
+ echo "usage: ${ME} update_dmg_mount_point" >& 2 |
+} |
+ |
+main() { |
+ local update_dmg_mount_point="${1}" |
+ |
+ # Early steps are critical. Don't continue past any failure. |
+ set -e |
+ |
+ trap cleanup EXIT HUP INT QUIT TERM |
+ |
+ readonly PRODUCT_NAME="Google Chrome" |
+ readonly APP_DIR="${PRODUCT_NAME}.app" |
+ readonly ALTERNATE_APP_DIR="${PRODUCT_NAME} Canary.app" |
+ readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework" |
+ readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" |
+ readonly PATCH_DIR=".patch" |
+ readonly CONTENTS_DIR="Contents" |
+ readonly APP_PLIST="${CONTENTS_DIR}/Info" |
+ readonly VERSIONS_DIR="${CONTENTS_DIR}/Versions" |
+ readonly UNROOTED_BRAND_PLIST="Library/Google/Google Chrome Brand" |
+ readonly UNROOTED_DEBUG_FILE="Library/Google/Google Chrome Updater Debug" |
+ |
+ readonly APP_VERSION_KEY="CFBundleShortVersionString" |
+ readonly APP_BUNDLEID_KEY="CFBundleIdentifier" |
+ 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" |
+ readonly KEYCHAIN_REAUTHORIZE_DIR=".keychain_reauthorize" |
+ |
+ # Don't use rsync -a, because -a expands to -rlptgoD. -g and -o copy owners |
+ # and groups, respectively, from the source, and that is undesirable in this |
+ # case. -D copies devices and special files; copying devices only works |
+ # when running as root, so for consistency between privileged and |
+ # unprivileged operation, this option is omitted as well. |
+ # -I, --ignore-times don't skip files that match in size and mod-time |
+ # -l, --links copy symlinks as symlinks |
+ # -r, --recursive recurse into directories |
+ # -p, --perms preserve permissions |
+ # -t, --times preserve times |
+ readonly RSYNC_FLAGS="-Ilprt" |
+ |
+ # It's difficult to get GOOGLE_CHROME_UPDATER_DEBUG set in the environment |
+ # when this script is called from Keystone. If a "debug file" exists in |
+ # either the root directory or the home directory of the user who owns the |
+ # ticket, turn on verbosity. This may aid debugging. |
+ if [[ -e "/${UNROOTED_DEBUG_FILE}" ]] || |
+ [[ -e ~/"${UNROOTED_DEBUG_FILE}" ]]; then |
+ export GOOGLE_CHROME_UPDATER_DEBUG="y" |
+ fi |
+ |
+ note "update_dmg_mount_point = ${update_dmg_mount_point}" |
+ |
+ # The argument should be the disk image path. Make sure it exists and that |
+ # it's an absolute path. |
+ note "checking update" |
+ |
+ if [[ -z "${update_dmg_mount_point}" ]] || |
+ [[ "${update_dmg_mount_point:0:1}" != "/" ]] || |
+ ! [[ -d "${update_dmg_mount_point}" ]]; then |
+ err "update_dmg_mount_point must be an absolute path to a directory" |
+ usage |
+ exit 2 |
+ fi |
+ |
+ local patch_dir="${update_dmg_mount_point}/${PATCH_DIR}" |
+ if [[ "${patch_dir:0:1}" != "/" ]]; then |
+ note "patch_dir = ${patch_dir}" |
+ err "patch_dir must be an absolute path" |
+ exit 2 |
+ fi |
+ |
+ # Figure out if this is an ordinary installation disk image being used as a |
+ # full update, or a patch. A patch will have a .patch directory at the root |
+ # of the disk image containing information about the update, tools to apply |
+ # it, and the update contents. |
+ local is_patch= |
+ local dirpatcher= |
+ if [[ -d "${patch_dir}" ]]; then |
+ # patch_dir exists and is a directory - this is a patch update. |
+ is_patch="y" |
+ dirpatcher="${patch_dir}/dirpatcher.sh" |
+ if ! [[ -x "${dirpatcher}" ]]; then |
+ err "couldn't locate dirpatcher" |
+ exit 6 |
+ fi |
+ elif [[ -e "${patch_dir}" ]]; then |
+ # patch_dir exists, but is not a directory - what's that mean? |
+ note "patch_dir = ${patch_dir}" |
+ err "patch_dir must be a directory" |
+ exit 2 |
+ else |
+ # patch_dir does not exist - this is a full "installer." |
+ patch_dir= |
+ fi |
+ note "patch_dir = ${patch_dir}" |
+ note "is_patch = ${is_patch}" |
+ note "dirpatcher = ${dirpatcher}" |
+ |
+ # The update to install. |
+ |
+ # update_app is the path to the new version of the .app. It will only be |
+ # set at this point for a non-patch update. It is not yet set for a patch |
+ # update because no such directory exists yet; it will be set later when |
+ # dirpatcher creates it. |
+ local update_app= |
+ |
+ # update_version_app_old, patch_app_dir, and patch_versioned_dir will only |
+ # be set for patch updates. |
+ local update_version_app_old= |
+ local patch_app_dir= |
+ local patch_versioned_dir= |
+ |
+ local update_version_app update_version_ks product_id |
+ if [[ -z "${is_patch}" ]]; then |
+ update_app="${update_dmg_mount_point}/${APP_DIR}" |
+ note "update_app = ${update_app}" |
+ |
+ # Make sure that it's an absolute path. |
+ if [[ "${update_app:0:1}" != "/" ]]; then |
+ err "update_app must be an absolute path" |
+ exit 2 |
+ fi |
+ |
+ # Make sure there's something to copy from. |
+ if ! [[ -d "${update_app}" ]]; then |
+ update_app="${update_dmg_mount_point}/${ALTERNATE_APP_DIR}" |
+ note "update_app = ${update_app}" |
+ |
+ if [[ "${update_app:0:1}" != "/" ]]; then |
+ err "update_app (alternate) must be an absolute path" |
+ exit 2 |
+ fi |
+ |
+ if ! [[ -d "${update_app}" ]]; then |
+ err "update_app must be a directory" |
+ exit 2 |
+ fi |
+ fi |
+ |
+ # Get some information about the update. |
+ note "reading update values" |
+ |
+ local update_app_plist="${update_app}/${APP_PLIST}" |
+ note "update_app_plist = ${update_app_plist}" |
+ if ! update_version_app="$(infoplist_read "${update_app_plist}" \ |
+ "${APP_VERSION_KEY}")" || |
+ [[ -z "${update_version_app}" ]]; then |
+ err "couldn't determine update_version_app" |
+ exit 2 |
+ fi |
+ note "update_version_app = ${update_version_app}" |
+ |
+ local update_ks_plist="${update_app_plist}" |
+ note "update_ks_plist = ${update_ks_plist}" |
+ if ! update_version_ks="$(infoplist_read "${update_ks_plist}" \ |
+ "${KS_VERSION_KEY}")" || |
+ [[ -z "${update_version_ks}" ]]; then |
+ err "couldn't determine update_version_ks" |
+ exit 2 |
+ fi |
+ note "update_version_ks = ${update_version_ks}" |
+ |
+ if ! product_id="$(infoplist_read "${update_ks_plist}" \ |
+ "${KS_PRODUCT_KEY}")" || |
+ [[ -z "${product_id}" ]]; then |
+ err "couldn't determine product_id" |
+ exit 2 |
+ fi |
+ note "product_id = ${product_id}" |
+ else # [[ -n "${is_patch}" ]] |
+ # Get some information about the update. |
+ note "reading update values" |
+ |
+ if ! update_version_app_old=$(<"${patch_dir}/old_app_version") || |
+ [[ -z "${update_version_app_old}" ]]; then |
+ err "couldn't determine update_version_app_old" |
+ exit 2 |
+ fi |
+ note "update_version_app_old = ${update_version_app_old}" |
+ |
+ if ! update_version_app=$(<"${patch_dir}/new_app_version") || |
+ [[ -z "${update_version_app}" ]]; then |
+ err "couldn't determine update_version_app" |
+ exit 2 |
+ fi |
+ note "update_version_app = ${update_version_app}" |
+ |
+ if ! update_version_ks=$(<"${patch_dir}/new_ks_version") || |
+ [[ -z "${update_version_ks}" ]]; then |
+ err "couldn't determine update_version_ks" |
+ exit 2 |
+ fi |
+ note "update_version_ks = ${update_version_ks}" |
+ |
+ if ! product_id=$(<"${patch_dir}/ks_product") || |
+ [[ -z "${product_id}" ]]; then |
+ err "couldn't determine product_id" |
+ exit 2 |
+ fi |
+ note "product_id = ${product_id}" |
+ |
+ patch_app_dir="${patch_dir}/application.dirpatch" |
+ if ! [[ -d "${patch_app_dir}" ]]; then |
+ err "couldn't locate patch_app_dir" |
+ exit 6 |
+ fi |
+ note "patch_app_dir = ${patch_app_dir}" |
+ |
+ patch_versioned_dir=\ |
+"${patch_dir}/version_${update_version_app_old}_${update_version_app}.dirpatch" |
+ if ! [[ -d "${patch_versioned_dir}" ]]; then |
+ err "couldn't locate patch_versioned_dir" |
+ exit 6 |
+ fi |
+ note "patch_versioned_dir = ${patch_versioned_dir}" |
+ fi |
+ |
+ # ksadmin is required. Keystone should have set a ${PATH} that includes it. |
+ # Check that here, so that more useful feedback can be offered in the |
+ # unlikely event that ksadmin is missing. |
+ note "checking Keystone" |
+ |
+ local ksadmin_path |
+ if ! ksadmin_path="$(type -p ksadmin)" || [[ -z "${ksadmin_path}" ]]; then |
+ err "couldn't locate ksadmin_path" |
+ exit 3 |
+ fi |
+ note "ksadmin_path = ${ksadmin_path}" |
+ |
+ # Call ksadmin_version once to prime the global state. This is needed |
+ # because subsequent calls to ksadmin_version that occur in $(...) |
+ # expansions will not affect the global state (although they can read from |
+ # the already-initialized global state) and thus will cause a new ksadmin |
+ # --ksadmin-version process to run for each check unless the globals have |
+ # been properly initialized beforehand. |
+ ksadmin_version >& /dev/null || true |
+ local ksadmin_version_string |
+ ksadmin_version_string="$(ksadmin_version 2> /dev/null || true)" |
+ note "ksadmin_version_string = ${ksadmin_version_string}" |
+ |
+ # Figure out where to install. |
+ local installed_app |
+ if ! installed_app="$(ksadmin -pP "${product_id}" | sed -Ene \ |
+ "s%^[[:space:]]+xc=<KSPathExistenceChecker:.* path=(/.+)>\$%\\1%p")" || |
+ [[ -z "${installed_app}" ]]; then |
+ err "couldn't locate installed_app" |
+ exit 3 |
+ fi |
+ note "installed_app = ${installed_app}" |
+ |
+ if [[ "${installed_app:0:1}" != "/" ]] || |
+ ! [[ -d "${installed_app}" ]]; then |
+ err "installed_app must be an absolute path to a directory" |
+ exit 3 |
+ fi |
+ |
+ # If this script is running as root, it's being driven by a system ticket. |
+ # Otherwise, it's being driven by a user ticket. |
+ local system_ticket= |
+ if [[ ${EUID} -eq 0 ]]; then |
+ system_ticket="y" |
+ fi |
+ note "system_ticket = ${system_ticket}" |
+ |
+ # If this script is being driven by a user ticket, but a system ticket is |
+ # also present, there's a potential for the two to collide. Both ticket |
+ # types might be present if another user on the system promoted the ticket |
+ # to system: the other user could not have removed this user's user ticket. |
+ # Handle that case here by deleting the user ticket and exiting early with |
+ # a discrete exit code. |
+ # |
+ # Current versions of ksadmin will exit 1 (false) when asked to print tickets |
+ # and given a specific product ID to print. Older versions of ksadmin would |
+ # exit 0 (true), but those same versions did not support -S (meaning to check |
+ # the system ticket store) and would exit 1 (false) with this invocation due |
+ # to not understanding the question. Therefore, the usage here will only |
+ # delete the existing user ticket when running as non-root with access to a |
+ # sufficiently recent ksadmin. Older ksadmins are tolerated: the update will |
+ # likely fail for another reason and the user ticket will hang around until |
+ # something is eventually able to remove it. |
+ if [[ -z "${system_ticket}" ]] && |
+ ksadmin -S --print-tickets --productid "${product_id}" >& /dev/null; then |
+ ksadmin --delete --productid "${product_id}" || true |
+ err "can't update on a user ticket when a system ticket is also present" |
+ exit 4 |
+ fi |
+ |
+ # Figure out what the existing installed application is using for its |
+ # versioned directory. This will be used later, to avoid removing the |
+ # existing installed version's versioned directory in case anything is still |
+ # using it. |
+ note "reading install values" |
+ |
+ local installed_app_plist="${installed_app}/${APP_PLIST}" |
+ note "installed_app_plist = ${installed_app_plist}" |
+ local installed_app_plist_path="${installed_app_plist}.plist" |
+ note "installed_app_plist_path = ${installed_app_plist_path}" |
+ local old_version_app |
+ old_version_app="$(infoplist_read "${installed_app_plist}" \ |
+ "${APP_VERSION_KEY}" || true)" |
+ note "old_version_app = ${old_version_app}" |
+ |
+ # old_version_app is not required, because it won't be present in skeleton |
+ # bootstrap installations, which just have an empty .app directory. Only |
+ # require it when doing a patch update, and use it to validate that the |
+ # patch applies to the old installed version. By definition, skeleton |
+ # bootstraps can't be installed with patch updates. They require the full |
+ # application on the disk image. |
+ if [[ -n "${is_patch}" ]]; then |
+ if [[ -z "${old_version_app}" ]]; then |
+ err "old_version_app required for patch" |
+ exit 6 |
+ elif [[ "${old_version_app}" != "${update_version_app_old}" ]]; then |
+ err "this patch does not apply to the installed version" |
+ exit 6 |
+ fi |
+ fi |
+ |
+ local installed_versions_dir="${installed_app}/${VERSIONS_DIR}" |
+ note "installed_versions_dir = ${installed_versions_dir}" |
+ |
+ # If the installed application is incredibly old, old_versioned_dir may not |
+ # exist. |
+ local old_versioned_dir |
+ if [[ -n "${old_version_app}" ]]; then |
+ old_versioned_dir="${installed_versions_dir}/${old_version_app}" |
+ fi |
+ note "old_versioned_dir = ${old_versioned_dir}" |
+ |
+ # Collect the installed application's brand code, it will be used later. It |
+ # is not an error for the installed application to not have a brand code. |
+ local old_ks_plist="${installed_app_plist}" |
+ note "old_ks_plist = ${old_ks_plist}" |
+ local old_brand |
+ old_brand="$(infoplist_read "${old_ks_plist}" \ |
+ "${KS_BRAND_KEY}" 2> /dev/null || |
+ true)" |
+ note "old_brand = ${old_brand}" |
+ |
+ ensure_writable_symlinks_recursive "${installed_app}" |
+ |
+ # By copying to ${installed_app}, the existing application name will be |
+ # preserved, if the user has renamed the application on disk. Respecting |
+ # the user's changes is friendly. |
+ |
+ # Make sure that ${installed_versions_dir} exists, so that it can receive |
+ # the versioned directory. It may not exist if updating from an older |
+ # version that did not use the versioned layout on disk. Later, during the |
+ # rsync to copy the application directory, the mode bits and timestamp on |
+ # ${installed_versions_dir} will be set to conform to whatever is present in |
+ # the update. |
+ # |
+ # ${installed_app} is guaranteed to exist at this point, but |
+ # ${installed_app}/${CONTENTS_DIR} may not if things are severely broken or |
+ # if this update is actually an initial installation from a Keystone |
+ # skeleton bootstrap. The mkdir creates ${installed_app}/${CONTENTS_DIR} if |
+ # it doesn't exist; its mode bits will be fixed up in a subsequent rsync. |
+ note "creating installed_versions_dir" |
+ if ! mkdir -p "${installed_versions_dir}"; then |
+ err "mkdir of installed_versions_dir failed" |
+ exit 5 |
+ fi |
+ |
+ local new_versioned_dir |
+ new_versioned_dir="${installed_versions_dir}/${update_version_app}" |
+ note "new_versioned_dir = ${new_versioned_dir}" |
+ |
+ # If there's an entry at ${new_versioned_dir} but it's not a directory |
+ # (or it's a symbolic link, whether or not it points to a directory), rsync |
+ # won't get rid of it. It's never correct to have a non-directory in place |
+ # of the versioned directory, so toss out whatever's there. Don't treat |
+ # this as a critical step: if removal fails, operation can still proceed to |
+ # to the dirpatcher or rsync, which will likely fail. |
+ if [[ -e "${new_versioned_dir}" ]] && |
+ ([[ -L "${new_versioned_dir}" ]] || |
+ ! [[ -d "${new_versioned_dir}" ]]); then |
+ note "removing non-directory in place of versioned directory" |
+ rm -f "${new_versioned_dir}" 2> /dev/null || true |
+ fi |
+ |
+ local update_versioned_dir |
+ if [[ -z "${is_patch}" ]]; then |
+ update_versioned_dir="${update_app}/${VERSIONS_DIR}/${update_version_app}" |
+ note "update_versioned_dir = ${update_versioned_dir}" |
+ else # [[ -n "${is_patch}" ]] |
+ # dirpatcher won't patch into a directory that already exists. Doing so |
+ # would be a bad idea, anyway. If ${new_versioned_dir} already exists, |
+ # it may be something left over from a previous failed or incomplete |
+ # update attempt, or it may be the live versioned directory if this is a |
+ # same-version update intended only to change channels. Since there's no |
+ # way to tell, this case is handled by having dirpatcher produce the new |
+ # versioned directory in a temporary location and then having rsync copy |
+ # it into place as an ${update_versioned_dir}, the same as in a non-patch |
+ # update. If ${new_versioned_dir} doesn't exist, dirpatcher can place the |
+ # new versioned directory at that location directly. |
+ local versioned_dir_target |
+ if ! [[ -e "${new_versioned_dir}" ]]; then |
+ versioned_dir_target="${new_versioned_dir}" |
+ note "versioned_dir_target = ${versioned_dir_target}" |
+ else |
+ ensure_temp_dir |
+ versioned_dir_target="${g_temp_dir}/${update_version_app}" |
+ note "versioned_dir_target = ${versioned_dir_target}" |
+ update_versioned_dir="${versioned_dir_target}" |
+ note "update_versioned_dir = ${update_versioned_dir}" |
+ fi |
+ |
+ note "dirpatching versioned directory" |
+ if ! "${dirpatcher}" "${old_versioned_dir}" \ |
+ "${patch_versioned_dir}" \ |
+ "${versioned_dir_target}"; then |
+ err "dirpatcher of versioned directory failed, status ${PIPESTATUS[0]}" |
+ exit 12 |
+ fi |
+ fi |
+ |
+ # Copy the versioned directory. The new versioned directory should have a |
+ # different name than any existing one, so this won't harm anything already |
+ # present in ${installed_versions_dir}, including the versioned directory |
+ # being used by any running processes. If this step is interrupted, there |
+ # will be an incomplete versioned directory left behind, but it won't |
+ # won't interfere with anything, and it will be replaced or removed during a |
+ # future update attempt. |
+ # |
+ # In certain cases, same-version updates are distributed to move users |
+ # between channels; when this happens, the contents of the versioned |
+ # directories are identical and rsync will not render the versioned |
+ # directory unusable even for an instant. |
+ # |
+ # ${update_versioned_dir} may be empty during a patch update (${is_patch}) |
+ # if the dirpatcher above was able to write it into place directly. In |
+ # that event, dirpatcher guarantees that ${new_versioned_dir} is already in |
+ # place. |
+ if [[ -n "${update_versioned_dir}" ]]; then |
+ note "rsyncing versioned directory" |
+ if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ |
+ "${new_versioned_dir}"; then |
+ err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" |
+ exit 7 |
+ fi |
+ fi |
+ |
+ if [[ -n "${is_patch}" ]]; then |
+ # If the versioned directory was prepared in a temporary directory and |
+ # then rsynced into place, remove the temporary copy now that it's no |
+ # longer needed. |
+ if [[ -n "${update_versioned_dir}" ]]; then |
+ rm -rf "${update_versioned_dir}" 2> /dev/null || true |
+ update_versioned_dir= |
+ note "update_versioned_dir = ${update_versioned_dir}" |
+ fi |
+ |
+ # Prepare ${update_app}. This always needs to be done in a temporary |
+ # location because dirpatcher won't write to a directory that already |
+ # exists, and ${installed_app} needs to be used as input to dirpatcher |
+ # in any event. The new application will be rsynced into place once |
+ # dirpatcher creates it. |
+ ensure_temp_dir |
+ update_app="${g_temp_dir}/${APP_DIR}" |
+ note "update_app = ${update_app}" |
+ |
+ note "dirpatching app directory" |
+ if ! "${dirpatcher}" "${installed_app}" \ |
+ "${patch_app_dir}" \ |
+ "${update_app}"; then |
+ err "dirpatcher of app directory failed, status ${PIPESTATUS[0]}" |
+ exit 13 |
+ fi |
+ fi |
+ |
+ # See if the timestamp of what's currently on disk is newer than the |
+ # update's outer .app's timestamp. rsync will copy the update's timestamp |
+ # over, but if that timestamp isn't as recent as what's already on disk, the |
+ # .app will need to be touched. |
+ local needs_touch= |
+ if [[ "${installed_app}" -nt "${update_app}" ]]; then |
+ needs_touch="y" |
+ fi |
+ note "needs_touch = ${needs_touch}" |
+ |
+ # Copy the unversioned files into place, leaving everything in |
+ # ${installed_versions_dir} alone. If this step is interrupted, the |
+ # application will at least remain in a usable state, although it may not |
+ # pass signature validation. Depending on when this step is interrupted, |
+ # the application will either launch the old or the new version. The |
+ # critical point is when the main executable is replaced. There isn't very |
+ # much to copy in this step, because most of the application is in the |
+ # versioned directory. This step only accounts for around 50 files, most of |
+ # which are small localized InfoPlist.strings files. Note that |
+ # ${VERSIONS_DIR} is included to copy its mode bits and timestamp, but its |
+ # contents are excluded, having already been installed above. |
+ note "rsyncing app directory" |
+ if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}/*" \ |
+ "${update_app}/" "${installed_app}"; then |
+ err "rsync of app directory failed, status ${PIPESTATUS[0]}" |
+ exit 8 |
+ fi |
+ |
+ note "rsyncs complete" |
+ |
+ if [[ -n "${is_patch}" ]]; then |
+ # update_app has been rsynced into place and is no longer needed. |
+ rm -rf "${update_app}" 2> /dev/null || true |
+ update_app= |
+ note "update_app = ${update_app}" |
+ fi |
+ |
+ if [[ -n "${g_temp_dir}" ]]; then |
+ # The temporary directory, if any, is no longer needed. |
+ rm -rf "${g_temp_dir}" 2> /dev/null || true |
+ g_temp_dir= |
+ note "g_temp_dir = ${g_temp_dir}" |
+ fi |
+ |
+ # 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 |
+ # bundle even if lsregister does not run. This is not done if rsync already |
+ # updated the timestamp to something newer than what had been on disk. This |
+ # is not considered a critical step, and if it fails, this script will not |
+ # exit. |
+ if [[ -n "${needs_touch}" ]]; then |
+ touch -cf "${installed_app}" || true |
+ fi |
+ |
+ # Read the new values, such as the version. |
+ note "reading new values" |
+ |
+ local new_version_app |
+ if ! new_version_app="$(infoplist_read "${installed_app_plist}" \ |
+ "${APP_VERSION_KEY}")" || |
+ [[ -z "${new_version_app}" ]]; then |
+ err "couldn't determine new_version_app" |
+ exit 9 |
+ fi |
+ note "new_version_app = ${new_version_app}" |
+ |
+ local new_versioned_dir="${installed_versions_dir}/${new_version_app}" |
+ note "new_versioned_dir = ${new_versioned_dir}" |
+ |
+ local new_ks_plist="${installed_app_plist}" |
+ note "new_ks_plist = ${new_ks_plist}" |
+ |
+ local new_version_ks |
+ if ! new_version_ks="$(infoplist_read "${new_ks_plist}" \ |
+ "${KS_VERSION_KEY}")" || |
+ [[ -z "${new_version_ks}" ]]; then |
+ err "couldn't determine new_version_ks" |
+ exit 9 |
+ fi |
+ note "new_version_ks = ${new_version_ks}" |
+ |
+ local update_url |
+ if ! update_url="$(infoplist_read "${new_ks_plist}" "${KS_URL_KEY}")" || |
+ [[ -z "${update_url}" ]]; then |
+ err "couldn't determine update_url" |
+ exit 9 |
+ fi |
+ note "update_url = ${update_url}" |
+ |
+ # The channel ID is optional. Suppress stderr to prevent Keystone from |
+ # seeing possible error output. |
+ local channel |
+ channel="$(infoplist_read "${new_ks_plist}" \ |
+ "${KS_CHANNEL_KEY}" 2> /dev/null || true)" |
+ note "channel = ${channel}" |
+ |
+ # 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 |
+ err "new_version_ks and update_version_ks do not match" |
+ exit 10 |
+ fi |
+ |
+ # Notify LaunchServices. This is not considered a critical step, and |
+ # lsregister's exit codes shouldn't be confused with this script's own. |
+ # Redirect stdout to /dev/null to suppress the useless "ThrottleProcessIO: |
+ # throttling disk i/o" messages that lsregister might print. |
+ note "notifying LaunchServices" |
+ local coreservices="/System/Library/Frameworks/CoreServices.framework" |
+ local launchservices="${coreservices}/Frameworks/LaunchServices.framework" |
+ local lsregister="${launchservices}/Support/lsregister" |
+ note "coreservices = ${coreservices}" |
+ note "launchservices = ${launchservices}" |
+ note "lsregister = ${lsregister}" |
+ "${lsregister}" -f "${installed_app}" > /dev/null || true |
+ |
+ # The brand information is stored differently depending on whether this is |
+ # running for a system or user ticket. |
+ note "handling brand code" |
+ |
+ local set_brand_file_access= |
+ local brand_plist |
+ if [[ -n "${system_ticket}" ]]; then |
+ # System ticket. |
+ set_brand_file_access="y" |
+ brand_plist="/${UNROOTED_BRAND_PLIST}" |
+ else |
+ # User ticket. |
+ brand_plist=~/"${UNROOTED_BRAND_PLIST}" |
+ fi |
+ local brand_plist_path="${brand_plist}.plist" |
+ note "set_brand_file_access = ${set_brand_file_access}" |
+ note "brand_plist = ${brand_plist}" |
+ note "brand_plist_path = ${brand_plist_path}" |
+ |
+ local ksadmin_brand_plist_path |
+ local ksadmin_brand_key |
+ |
+ # Only the stable channel, identified by an empty channel string, has a |
+ # brand code. On the beta and dev channels, remove the brand plist if |
+ # present. Its presence means that the ticket used to manage a |
+ # stable-channel Chrome but the user has since replaced it with a beta or |
+ # dev channel version. Since the canary channel can run side-by-side with |
+ # another Chrome installation, don't remove the brand plist on that channel, |
+ # but skip the rest of the brand logic. |
+ if [[ "${channel}" = "beta" ]] || [[ "${channel}" = "dev" ]]; then |
+ note "defeating brand code on channel ${channel}" |
+ rm -f "${brand_plist_path}" 2>/dev/null || true |
+ elif [[ -n "${channel}" ]]; then |
+ # Canary channel. |
+ note "skipping brand code on channel ${channel}" |
+ else |
+ # Stable channel. |
+ # If the user manually updated their copy of Chrome, there might be new |
+ # brand information in the app bundle, and that needs to be copied out |
+ # into the file Keystone looks at. |
+ if [[ -n "${old_brand}" ]]; then |
+ local brand_dir |
+ brand_dir="$(dirname "${brand_plist_path}")" |
+ note "brand_dir = ${brand_dir}" |
+ if ! mkdir -p "${brand_dir}"; then |
+ err "couldn't mkdir brand_dir, continuing" |
+ else |
+ if ! defaults write "${brand_plist}" "${KS_BRAND_KEY}" \ |
+ -string "${old_brand}"; then |
+ err "couldn't write brand_plist, continuing" |
+ elif [[ -n "${set_brand_file_access}" ]]; then |
+ if ! chown "root:wheel" "${brand_plist_path}"; then |
+ err "couldn't chown brand_plist_path, continuing" |
+ else |
+ if ! chmod 644 "${brand_plist_path}"; then |
+ err "couldn't chmod brand_plist_path, continuing" |
+ fi |
+ fi |
+ fi |
+ fi |
+ fi |
+ |
+ # Confirm that the brand file exists. It's optional. |
+ ksadmin_brand_plist_path="${brand_plist_path}" |
+ ksadmin_brand_key="${KS_BRAND_KEY}" |
+ |
+ if [[ ! -f "${ksadmin_brand_plist_path}" ]]; then |
+ # Clear any branding information. |
+ ksadmin_brand_plist_path= |
+ ksadmin_brand_key= |
+ fi |
+ fi |
+ |
+ note "ksadmin_brand_plist_path = ${ksadmin_brand_plist_path}" |
+ note "ksadmin_brand_key = ${ksadmin_brand_key}" |
+ |
+ note "notifying Keystone" |
+ |
+ local ksadmin_args=( |
+ --register |
+ --productid "${product_id}" |
+ --version "${new_version_ks}" |
+ --xcpath "${installed_app}" |
+ --url "${update_url}" |
+ ) |
+ |
+ if ksadmin_supports_tag; then |
+ ksadmin_args+=( |
+ --tag "${channel}" |
+ ) |
+ fi |
+ |
+ if ksadmin_supports_tagpath_tagkey; then |
+ ksadmin_args+=( |
+ --tag-path "${installed_app_plist_path}" |
+ --tag-key "${KS_CHANNEL_KEY}" |
+ ) |
+ fi |
+ |
+ if ksadmin_supports_brandpath_brandkey; then |
+ ksadmin_args+=( |
+ --brand-path "${ksadmin_brand_plist_path}" |
+ --brand-key "${ksadmin_brand_key}" |
+ ) |
+ fi |
+ |
+ if ksadmin_supports_versionpath_versionkey; then |
+ ksadmin_args+=( |
+ --version-path "${installed_app_plist_path}" |
+ --version-key "${KS_VERSION_KEY}" |
+ ) |
+ fi |
+ |
+ note "ksadmin_args = ${ksadmin_args[*]}" |
+ |
+ if ! ksadmin "${ksadmin_args[@]}"; then |
+ err "ksadmin failed" |
+ exit 11 |
+ fi |
+ |
+ # The remaining steps are not considered critical. |
+ set +e |
+ |
+ # Try to clean up old versions that are not in use. The strategy is to keep |
+ # the versioned directory corresponding to the update just applied |
+ # (obviously) and the version that was just replaced, and to use ps and lsof |
+ # to see if it looks like any processes are currently using any other old |
+ # directories. Directories not in use are removed. Old versioned |
+ # directories that are in use are left alone so as to not interfere with |
+ # running processes. These directories can be cleaned up by this script on |
+ # future updates. |
+ # |
+ # To determine which directories are in use, both ps and lsof are used. |
+ # Each approach has limitations. |
+ # |
+ # The ps check looks for processes within the versioned directory. Only |
+ # helper processes, such as renderers, are within the versioned directory. |
+ # Browser processes are not, so the ps check will not find them, and will |
+ # assume that a versioned directory is not in use if a browser is open |
+ # without any windows. The ps mechanism can also only detect processes |
+ # running on the system that is performing the update. If network shares |
+ # are involved, all bets are off. |
+ # |
+ # The lsof check looks to see what processes have the framework dylib open. |
+ # Browser processes will have their versioned framework dylib open, so this |
+ # check is able to catch browsers even if there are no associated helper |
+ # processes. Like the ps check, the lsof check is limited to processes on |
+ # the system that is performing the update. Finally, unless running as |
+ # root, the lsof check can only find processes running as the effective user |
+ # performing the update. |
+ # |
+ # These limitations are motivations to additionally preserve the versioned |
+ # directory corresponding to the version that was just replaced. |
+ note "cleaning up old versioned directories" |
+ |
+ local versioned_dir |
+ for versioned_dir in "${installed_versions_dir}/"*; do |
+ note "versioned_dir = ${versioned_dir}" |
+ if [[ "${versioned_dir}" = "${new_versioned_dir}" ]] || \ |
+ [[ "${versioned_dir}" = "${old_versioned_dir}" ]]; then |
+ # This is the versioned directory corresponding to the update that was |
+ # just applied or the version that was previously in use. Leave it |
+ # alone. |
+ note "versioned_dir is new_versioned_dir or old_versioned_dir, skipping" |
+ continue |
+ fi |
+ |
+ # Look for any processes whose executables are within this versioned |
+ # directory. They'll be helper processes, such as renderers. Their |
+ # existence indicates that this versioned directory is currently in use. |
+ local ps_string="${versioned_dir}/" |
+ note "ps_string = ${ps_string}" |
+ |
+ # Look for any processes using the framework dylib. This will catch |
+ # browser processes where the ps check will not, but it is limited to |
+ # processes running as the effective user. |
+ local lsof_file="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}" |
+ note "lsof_file = ${lsof_file}" |
+ |
+ # ps -e displays all users' processes, -ww causes ps to not truncate |
+ # lines, -o comm instructs it to only print the command name, and the = |
+ # tells it to not print a header line. |
+ # The cut invocation filters the ps output to only have at most the number |
+ # of characters in ${ps_string}. This is done so that grep can look for |
+ # an exact match. |
+ # grep -F tells grep to look for lines that are exact matches (not regular |
+ # expressions), -q tells it to not print any output and just indicate |
+ # matches by exit status, and -x tells it that the entire line must match |
+ # ${ps_string} exactly, as opposed to matching a substring. A match |
+ # causes grep to exit zero (true). |
+ # |
+ # lsof will exit nonzero if ${lsof_file} does not exist or is open by any |
+ # process. If the file exists and is open, it will exit zero (true). |
+ if (! ps -ewwo comm= | \ |
+ cut -c "1-${#ps_string}" | \ |
+ grep -Fqx "${ps_string}") && |
+ (! lsof "${lsof_file}" >& /dev/null); then |
+ # It doesn't look like anything is using this versioned directory. Get |
+ # rid of it. |
+ note "versioned_dir doesn't appear to be in use, removing" |
+ rm -rf "${versioned_dir}" |
+ else |
+ note "versioned_dir is in use, skipping" |
+ fi |
+ done |
+ |
+ # If this script is being driven by a user Keystone ticket, it is not |
+ # running as root. If the application is installed somewhere under |
+ # /Applications, try to make it writable by all admin users. This will |
+ # allow other admin users to update the application from their own user |
+ # Keystone instances. |
+ # |
+ # If the script is being driven by a user Keystone ticket (not running as |
+ # root) and the application is not installed under /Applications, it might |
+ # not be in a system-wide location, and it probably won't be something that |
+ # other users on the system are running, so err on the side of safety and |
+ # don't make it group-writable. |
+ # |
+ # If this script is being driven by a system ticket (running as root), it's |
+ # future updates can be expected to be applied the same way, so admin- |
+ # writability is not a concern. Set the entire thing to be owned by root |
+ # in that case, regardless of where it's installed, and drop any group and |
+ # other write permission. |
+ # |
+ # If this script is running as a user that is not a member of the admin |
+ # group, the chgrp operation will not succeed. Tolerate that case, because |
+ # it's better than the alternative, which is to make the application |
+ # world-writable. |
+ note "setting permissions" |
+ |
+ local chmod_mode="a+rX,u+w,go-w" |
+ if [[ -z "${system_ticket}" ]]; then |
+ if [[ "${installed_app:0:14}" = "/Applications/" ]] && |
+ chgrp -Rh admin "${installed_app}" 2> /dev/null; then |
+ chmod_mode="a+rX,ug+w,o-w" |
+ fi |
+ else |
+ chown -Rh root:wheel "${installed_app}" 2> /dev/null |
+ fi |
+ |
+ note "chmod_mode = ${chmod_mode}" |
+ chmod -R "${chmod_mode}" "${installed_app}" 2> /dev/null |
+ |
+ # On the Mac, or at least on HFS+, symbolic link permissions are significant, |
+ # but chmod -R and -h can't be used together. Do another pass to fix the |
+ # permissions on any symbolic links. |
+ find "${installed_app}" -type l -exec chmod -h "${chmod_mode}" {} + \ |
+ 2> /dev/null |
+ |
+ # If an update is triggered from within the application itself, the update |
+ # process inherits the quarantine bit (LSFileQuarantineEnabled). Any files |
+ # or directories created during the update will be quarantined in that case, |
+ # which may cause Launch Services to display quarantine UI. That's bad, |
+ # especially if it happens when the outer .app launches a quarantined inner |
+ # helper. If the application is already on the system and is being updated, |
+ # then it can be assumed that it should not be quarantined. Use xattr to |
+ # drop the quarantine attribute. |
+ # |
+ # TODO(mark): Instead of letting the quarantine attribute be set and then |
+ # dropping it here, figure out a way to get the update process to run |
+ # without LSFileQuarantineEnabled even when triggering an update from within |
+ # the application. |
+ note "lifting quarantine" |
+ |
+ if os_xattr_supports_r; then |
+ # On 10.6, xattr supports -r for recursive operation. |
+ xattr -d -r "${QUARANTINE_ATTR}" "${installed_app}" 2> /dev/null |
+ else |
+ # On earlier systems, xattr doesn't support -r, so run xattr via find. |
+ find "${installed_app}" -exec xattr -d "${QUARANTINE_ATTR}" {} + \ |
+ 2> /dev/null |
+ fi |
+ |
+ # Do Keychain reauthorization. This involves running a stub executable on |
+ # the dmg that loads the newly-updated framework and jumps to it to perform |
+ # the reauthorization. The stub executable can be signed by the old |
+ # certificate even after the rest of Chrome switches to the new certificate, |
+ # so it still has access to the old Keychain items. The stub executable is |
+ # an unbundled flat file executable whose name matches the real |
+ # application's bundle identifier, so it's permitted access to the Keychain |
+ # items. Doing a reauthorization step at update time reauthorizes Keychain |
+ # items for users who never bother restarting Chrome, and provides a |
+ # mechanism to continue doing reauthorizations even after the certificate |
+ # changes. However, it only works for non-system ticket installations of |
+ # Chrome, because the updater runs as root when on a system ticket, and root |
+ # can't access individual user Keychains. |
+ # |
+ # Even if the reauthorization tool is launched, it doesn't necessarily try |
+ # to do anything. It will only attempt to perform a reauthorization if one |
+ # hasn't yet been done at update time. |
+ note "maybe reauthorizing Keychain" |
+ |
+ if [[ -z "${system_ticket}" ]]; then |
+ local new_bundleid_app |
+ new_bundleid_app="$(infoplist_read "${installed_app_plist}" \ |
+ "${APP_BUNDLEID_KEY}" || true)" |
+ note "new_bundleid_app = ${new_bundleid_app}" |
+ |
+ local keychain_reauthorize_dir="\ |
+${update_dmg_mount_point}/${KEYCHAIN_REAUTHORIZE_DIR}" |
+ local keychain_reauthorize_path="\ |
+${keychain_reauthorize_dir}/${new_bundleid_app}" |
+ note "keychain_reauthorize_path = ${keychain_reauthorize_path}" |
+ |
+ if [[ -x "${keychain_reauthorize_path}" ]]; then |
+ local framework_dir="${new_versioned_dir}/${FRAMEWORK_DIR}" |
+ local framework_code_path="${framework_dir}/${FRAMEWORK_NAME}" |
+ note "framework_code_path = ${framework_code_path}" |
+ |
+ if [[ -f "${framework_code_path}" ]]; then |
+ note "reauthorizing Keychain" |
+ "${keychain_reauthorize_path}" "${framework_code_path}" |
+ fi |
+ fi |
+ else |
+ note "system ticket, not reauthorizing Keychain" |
+ fi |
+ |
+ # Great success! |
+ note "done!" |
+ |
+ trap - EXIT |
+ |
+ return 0 |
+} |
+ |
+# Check "less than" instead of "not equal to" in case Keystone ever changes to |
+# pass more arguments. |
+if [[ ${#} -lt 1 ]]; then |
+ usage |
+ exit 2 |
+fi |
+ |
+main "${@}" |
+exit ${?} |
Property changes on: chrome_mac/Google Chrome Packaging/keystone_install.sh |
___________________________________________________________________ |
Added: svn:executable |
+ * |