summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-06-14 21:08:02 +0000
committermark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-06-14 21:08:02 +0000
commit15b1c17f372dfe21fd5306ece4676a8c914b3e34 (patch)
tree3ee444983b9e4ad344eee8e4c7d2fb2ea3882b58
parent14593524ba43e80db1c842df87026d0194ff823d (diff)
downloadchromium_src-15b1c17f372dfe21fd5306ece4676a8c914b3e34.zip
chromium_src-15b1c17f372dfe21fd5306ece4676a8c914b3e34.tar.gz
chromium_src-15b1c17f372dfe21fd5306ece4676a8c914b3e34.tar.bz2
Adapt keystone_install.sh to the shell scripting guidelines. Tidy it up a bit.
BUG=45017 TEST=none Review URL: http://codereview.chromium.org/2821001 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@49726 0039d316-1c4b-4281-b951-d872f2087c98
-rwxr-xr-xchrome/installer/mac/keystone_install.sh1392
1 files changed, 855 insertions, 537 deletions
diff --git a/chrome/installer/mac/keystone_install.sh b/chrome/installer/mac/keystone_install.sh
index 9465e64..2d24d46 100755
--- a/chrome/installer/mac/keystone_install.sh
+++ b/chrome/installer/mac/keystone_install.sh
@@ -1,27 +1,36 @@
-#!/bin/bash
+#!/bin/bash -p
# Copyright (c) 2009 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.
-
-# Return values:
-# 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 rsync failed (could not assure presence of Versions directory)
-# 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
-
-set -e
+#
+# 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 rsync failed (could not assure presence of Versions directory)
+# 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
+
+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
@@ -30,25 +39,64 @@ set -e
# sbin directory. Adjust the path accordingly.
export PATH="${PATH}:/sbin:/usr/sbin"
-# Returns 0 (true) if the parameter exists, is a symbolic link, and appears
+# 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
+
+declare 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
+}
+
+# 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
-# writeability like test's -w primary, but -w resolves symbolic links and this
+# writability like test's -w primary, but -w resolves symbolic links and this
# function does not.
-function is_writable_symlink() {
- SYMLINK=${1}
- LINKMODE=$(stat -f %Sp "${SYMLINK}" 2> /dev/null || true)
- if [ -z "${LINKMODE}" ] || [ "${LINKMODE:0:1}" != "l" ] ; then
+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
- LINKUSER=$(stat -f %u "${SYMLINK}" 2> /dev/null || true)
- LINKGROUP=$(stat -f %g "${SYMLINK}" 2> /dev/null || true)
- if [ -z "${LINKUSER}" ] || [ -z "${LINKGROUP}" ] ; then
+
+ 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 ${LINKUSER} ] ; then
- if [ "${LINKMODE:2:1}" = "w" ] ; then
+ if [[ ${EUID} -eq "${link_user}" ]]; then
+ if [[ "${link_mode:2:1}" = "w" ]]; then
return 0
fi
return 1
@@ -56,36 +104,39 @@ function is_writable_symlink() {
# If the file's group matches any of the groups that this process is a
# member of, check the group-write bit.
- GROUPMATCH=
- for group in ${GROUPS[@]} ; do
- if [ ${group} -eq ${LINKGROUP} ] ; then
- GROUPMATCH=1
+ local group_match=
+ local group
+ for group in "${GROUPS[@]}"; do
+ if [[ "${group}" -eq "${link_group}" ]]; then
+ group_match="y"
break
fi
done
- if [ -n "${GROUPMATCH}" ] ; then
- if [ "${LINKMODE:5:1}" = "w" ] ; then
+ if [[ -n "${group_match}" ]]; then
+ if [[ "${link_mode:5:1}" = "w" ]]; then
return 0
fi
return 1
fi
# Check the other-write bit.
- if [ "${LINKMODE:8:1}" = "w" ] ; then
+ 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
+# 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 FROM does not exist, is not a symbolic link, or
-# is already writable, this function does nothing. This function always
-# returns 0 (true).
-function ensure_writable_symlink() {
- SYMLINK=${1}
- if [ -L "${SYMLINK}" ] && ! is_writable_symlink "${SYMLINK}" ; then
- # If ${SYMLINK} refers to a directory, doing this naively might result in
+# 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
@@ -101,23 +152,33 @@ function ensure_writable_symlink() {
# oldlink, newlink must have the same basename, hence the temporary
# directory.
- TARGET=$(readlink "${SYMLINK}" 2> /dev/null || true)
- if [ -z "${TARGET}" ] ; then
+ local target
+ target="$(readlink "${symlink}" 2> /dev/null || true)"
+ if [[ -z "${target}" ]]; then
return 0
fi
- SYMLINKDIR=$(dirname "${SYMLINK}")
- TEMPLINKDIR="${SYMLINKDIR}/.symlink_temp.${$}.${RANDOM}"
- TEMPLINK="${TEMPLINKDIR}/$(basename "${SYMLINK}")"
+ # 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}")"
- # Don't bail out here if this fails. Something else will probably fail.
- # Let it, it'll probably be easier to understand that failure than this
- # one.
- (mkdir "${TEMPLINKDIR}" && \
- ln -fhs "${TARGET}" "${TEMPLINK}" && \
- chmod -h 755 "${TEMPLINK}" && \
- mv -f "${TEMPLINK}" "${SYMLINKDIR}") || true
- rm -rf "${TEMPLINKDIR}"
+ (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
@@ -128,536 +189,793 @@ function ensure_writable_symlink() {
# 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=
-function ksadmin_version() {
- if [ -z "${G_CHECKED_KSADMIN_VERSION}" ] ; then
- G_CHECKED_KSADMIN_VERSION=1
- G_KSADMIN_VERSION=$(ksadmin --ksadmin-version || true)
- fi
- echo "${G_KSADMIN_VERSION}"
+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)"
+ fi
+ echo "${g_ksadmin_version}"
return 0
}
# Compares the installed ksadmin version against a supplied version number,
-# and returns 0 (true) if the number to check is the same as or newer than the
-# installed Keystone. Returns 1 (false) if the installed Keystone version
-# number cannot be determined or if the number to check is less than the
-# installed Keystone. The check argument should be a string of the form
-# "major.minor.micro.build".
-function is_ksadmin_version_ge() {
- CHECK_VERSION=${1}
- KSADMIN_VERSION=$(ksadmin_version)
- if [ -n "${KSADMIN_VERSION}" ] ; then
- VER_RE='^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$'
-
- KSADMIN_VERSION_MAJOR=$(sed -Ene "s/${VER_RE}/\1/p" <<< ${KSADMIN_VERSION})
- KSADMIN_VERSION_MINOR=$(sed -Ene "s/${VER_RE}/\2/p" <<< ${KSADMIN_VERSION})
- KSADMIN_VERSION_MICRO=$(sed -Ene "s/${VER_RE}/\3/p" <<< ${KSADMIN_VERSION})
- KSADMIN_VERSION_BUILD=$(sed -Ene "s/${VER_RE}/\4/p" <<< ${KSADMIN_VERSION})
-
- CHECK_VERSION_MAJOR=$(sed -Ene "s/${VER_RE}/\1/p" <<< ${CHECK_VERSION})
- CHECK_VERSION_MINOR=$(sed -Ene "s/${VER_RE}/\2/p" <<< ${CHECK_VERSION})
- CHECK_VERSION_MICRO=$(sed -Ene "s/${VER_RE}/\3/p" <<< ${CHECK_VERSION})
- CHECK_VERSION_BUILD=$(sed -Ene "s/${VER_RE}/\4/p" <<< ${CHECK_VERSION})
-
- if [ ${KSADMIN_VERSION_MAJOR} -gt ${CHECK_VERSION_MAJOR} ] ||
- ([ ${KSADMIN_VERSION_MAJOR} -eq ${CHECK_VERSION_MAJOR} ] && (
- [ ${KSADMIN_VERSION_MINOR} -gt ${CHECK_VERSION_MINOR} ] ||
- ([ ${KSADMIN_VERSION_MINOR} -eq ${CHECK_VERSION_MINOR} ] && (
- [ ${KSADMIN_VERSION_MICRO} -gt ${CHECK_VERSION_MICRO} ] ||
- ([ ${KSADMIN_VERSION_MICRO} -eq ${CHECK_VERSION_MICRO} ] &&
- [ ${KSADMIN_VERSION_BUILD} -ge ${CHECK_VERSION_BUILD} ])
- ))
- )) ; then
+# |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". Returns 1 (false) if either |check_version| or
+# the Keystone version do not match this format.
+readonly VER_RE="^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)\$"
+is_ksadmin_version_ge() {
+ local check_version="${1}"
+
+ if ! [[ "${check_version}" =~ ${VER_RE} ]]; then
+ return 1
+ fi
+
+ local check_components=("${BASH_REMATCH[1]}"
+ "${BASH_REMATCH[2]}"
+ "${BASH_REMATCH[3]}"
+ "${BASH_REMATCH[4]}")
+
+ local ksadmin_version
+ ksadmin_version="$(ksadmin_version)"
+
+ if ! [[ "${ksadmin_version}" =~ ${VER_RE} ]]; then
+ return 1
+ fi
+
+ local ksadmin_components=("${BASH_REMATCH[1]}"
+ "${BASH_REMATCH[2]}"
+ "${BASH_REMATCH[3]}"
+ "${BASH_REMATCH[4]}")
+
+ local i
+ for i in 0 1 2 3; do
+ local check_component="${check_components[${i}]}"
+ local ksadmin_component="${ksadmin_components[${i}]}"
+
+ if [[ ${ksadmin_component} -lt ${check_component} ]]; then
+ # ksadmin_version is less than check_version
+ return 1
+ fi
+ if [[ ${ksadmin_component} -gt ${check_component} ]]; then
+ # ksadmin_version is greater than check_version
return 0
fi
- fi
+ done
- return 1
+ # The version numbers are equal.
+ return 0
}
# Returns 0 (true) if ksadmin supports --tag.
-function ksadmin_supports_tag() {
- KSADMIN_VERSION=$(ksadmin_version)
- if [ -n "${KSADMIN_VERSION}" ] ; then
+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.
-function ksadmin_supports_tagpath_tagkey() {
+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 --tag-path, --tag-key, --brand-path,
-# and --brand-key.
-function ksadmin_supports_brandpath_brandkey() {
+# 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.
- # --tag-path and --tag-key are already supported if the brand arguments are
- # also supported.
is_ksadmin_version_ge 1.0.8.1620
+
# The return value of is_ksadmin_version_ge is used as this function's
# return value.
}
-# The argument should be the disk image path. Make sure it exists.
-if [ $# -lt 1 ] || [ ! -d "${1}" ]; then
- exit 2
-fi
+usage() {
+ echo "usage: ${ME} update_dmg_mount_point" >& 2
+}
-# Who we are.
-PRODUCT_NAME="Google Chrome"
-APP_DIR="${PRODUCT_NAME}.app"
-FRAMEWORK_NAME="${PRODUCT_NAME} Framework"
-FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework"
-SRC="${1}/${APP_DIR}"
+main() {
+ local update_dmg_mount_point="${1}"
-# Make sure that there's something to copy from, and that it's an absolute
-# path.
-if [ -z "${SRC}" ] || [ "${SRC:0:1}" != "/" ] || [ ! -d "${SRC}" ] ; then
- exit 2
-fi
+ # Early steps are critical. Don't continue past any failure.
+ set -e
-# Figure out where we're going. Determine the application version to be
-# installed, use that to locate the framework, and then look inside the
-# framework for the Keystone product ID.
-APP_VERSION_KEY="CFBundleShortVersionString"
-UPD_VERSION_APP=$(defaults read "${SRC}/Contents/Info" "${APP_VERSION_KEY}" ||
- exit 2)
-UPD_KS_PLIST="${SRC}/Contents/Info"
-KS_VERSION_KEY="KSVersion"
-UPD_VERSION_KS=$(defaults read "${UPD_KS_PLIST}" "${KS_VERSION_KEY}" || exit 2)
-PRODUCT_ID=$(defaults read "${UPD_KS_PLIST}" KSProductID || exit 2)
-if [ -z "${UPD_VERSION_KS}" ] || [ -z "${PRODUCT_ID}" ] ; then
- exit 3
-fi
-DEST=$(ksadmin -pP "${PRODUCT_ID}" |
- sed -Ene \
- 's%^[[:space:]]+xc=<KSPathExistenceChecker:.* path=(/.+)>$%\1%p')
+ readonly PRODUCT_NAME="Google Chrome"
+ readonly APP_DIR="${PRODUCT_NAME}.app"
+ readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework"
+ readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework"
+ 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 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"
+
+ # 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
-# More sanity checking.
-if [ -z "${DEST}" ] || [ ! -d "${DEST}" ]; then
- exit 3
-fi
+ note "update_dmg_mount_point = ${update_dmg_mount_point}"
-# If this script is not running as root, it's being driven by a user ticket.
-# If 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 status.
-#
-# 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 [ ${EUID} -ne 0 ] &&
- ksadmin -S --print-tickets -P "${PRODUCT_ID}" >& /dev/null ; then
- ksadmin --delete -P "${PRODUCT_ID}" || true
- exit 4
-fi
+ # The argument should be the disk image path. Make sure it exists and that
+ # it's an absolute path.
+ note "checking update"
-# Figure out what the existing version is using for its versioned directory.
-# This will be used later, to avoid removing the currently-installed version's
-# versioned directory in case anything is still using it.
-OLD_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" ||
- true)
-OLD_VERSIONED_DIR="${DEST}/Contents/Versions/${OLD_VERSION_APP}"
-
-# 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.
-NEEDS_TOUCH=
-if [ "${DEST}" -nt "${SRC}" ] ; then
- NEEDS_TOUCH=1
-fi
+ 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
-# 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.
-#
-# This fix-up is not necessary when running as root, because root will always
-# be able to write everything needed.
-#
-# 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 whateer 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.
-if [ ${EUID} -ne 0 ] ; then
- # This step isn't critical.
- set +e
+ # The update to install.
+ local update_app="${update_dmg_mount_point}/${APP_DIR}"
+ note "update_app = ${update_app}"
- # Reset ${IFS} to deal with spaces in the for loop by not breaking the
- # list up when they're encountered.
- IFS_OLD="${IFS}"
- IFS=$(printf '\n\t')
+ # Make sure that there's something to copy from, and that it's an absolute
+ # path.
+ if [[ "${update_app:0:1}" != "/" ]] ||
+ ! [[ -d "${update_app}" ]]; then
+ err "update_app must be an absolute path to a directory"
+ exit 2
+ fi
- # Only consider symbolic links in ${SRC}. If there are any other links in
- # ${DEST} not present in ${SRC}, rsync will delete them as needed later.
- LINKS=$(cd "${SRC}" && find . -type l)
+ # 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}"
+ local update_version_app
+ if ! update_version_app="$(defaults 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}"
+ local update_version_ks
+ if ! update_version_ks="$(defaults 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}"
+
+ local product_id
+ if ! product_id="$(defaults 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}"
- for link in ${LINKS} ; do
- # ${link} is relative to ${SRC}. Prepending ${DEST} looks for the same
- # link already on disk.
- DESTLINK="${DEST}/${link}"
- ensure_writable_symlink "${DESTLINK}"
- done
+ # 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"
- # Go back to how things were.
- IFS="${IFS_OLD}"
- set -e
-fi
+ 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}"
-# Collect the current app brand, it will be use later.
-BRAND_ID_KEY=KSBrandID
-APP_BRAND=$(defaults read "${DEST}/Contents/Info" "${BRAND_ID_KEY}" 2>/dev/null ||
- true)
-
-# 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
-RSYNC_FLAGS="-Ilprt"
-
-# By copying to ${DEST}, the existing application name will be preserved, even
-# if the user has renamed the application on disk. Respecting the user's
-# changes is friendly.
-
-# Make sure that the Versions directory 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. An rsync that excludes all
-# contents is used to bring the permissions over from the update's Versions
-# directory, otherwise, this directory would be the only one in the entire
-# update exempt from getting its permissions copied over. A simple mkdir
-# wouldn't copy mode bits. This is done even if ${DEST}/Contents/Versions
-# already does exist to ensure that the mode bits come from the update.
-#
-# ${DEST} is guaranteed to exist at this point, but ${DEST}/Contents may not
-# if things are severely broken or if this update is actually an initial
-# installation from a Keystone skeleton bootstrap. The mkdir creates
-# ${DEST}/Contents if it doesn't exist; its mode bits will be fixed up in a
-# subsequent rsync.
-mkdir -p "${DEST}/Contents" || exit 5
-rsync ${RSYNC_FLAGS} --exclude "*" "${SRC}/Contents/Versions/" \
- "${DEST}/Contents/Versions" || exit 6
-
-# Copy the versioned directory. The new versioned directory will have a
-# different name than any existing one, so this won't harm anything already
-# present in Contents/Versions, 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 interfere with
-# anything, and it will be replaced or removed during a future update attempt.
-NEW_VERSIONED_DIR="${DEST}/Contents/Versions/${UPD_VERSION_APP}"
-rsync ${RSYNC_FLAGS} --delete-before \
- "${SRC}/Contents/Versions/${UPD_VERSION_APP}/" \
- "${NEW_VERSIONED_DIR}" || exit 7
-
-# Copy the unversioned files into place, leaving everything in
-# Contents/Versions 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.
-rsync ${RSYNC_FLAGS} --delete-after --exclude /Contents/Versions \
- "${SRC}/" "${DEST}" || exit 8
-
-# 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 "${DEST}" || true
-fi
+ if [[ "${installed_app:0:1}" != "/" ]] ||
+ ! [[ -d "${installed_app}" ]]; then
+ err "installed_app must be an absolute path to a directory"
+ exit 3
+ fi
-# Read the new values (e.g. version). Get the installed application version
-# to get the path to the framework, where the Keystone keys are stored.
-NEW_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" ||
- exit 9)
-NEW_KS_PLIST="${DEST}/Contents/Info"
-NEW_VERSION_KS=$(defaults read "${NEW_KS_PLIST}" "${KS_VERSION_KEY}" || exit 9)
-URL=$(defaults read "${NEW_KS_PLIST}" KSUpdateURL || exit 9)
-# The channel ID is optional. Suppress stderr to prevent Keystone from seeing
-# possible error output.
-CHANNEL_ID_KEY=KSChannelID
-CHANNEL_ID=$(defaults read "${NEW_KS_PLIST}" "${CHANNEL_ID_KEY}" 2>/dev/null ||
- true)
-
-# 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}" != "${UPD_VERSION_KS}" ]; then
- exit 10
-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 -P "${product_id}" >& /dev/null; then
+ ksadmin --delete -P "${product_id}" || true
+ err "can't update on a user ticket when a system ticket is also present"
+ exit 4
+ fi
-# Notify LaunchServices. This is not considered a critical step, and
-# lsregister's exit codes shouldn't be confused with this script's own.
-/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister "${DEST}" || true
-
-# 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
-
-# The brand information is stored differently depending on whether this is
-# running for a system or user ticket.
-BRAND_PATH_PLIST=
-SET_BRAND_FILE_ACCESS=no
-if [ ${EUID} -ne 0 ] ; then
- # Using a user level ticket.
- BRAND_PATH_PLIST=~/"Library/Google/Google Chrome Brand"
-else
- # Using a system level ticket.
- BRAND_PATH_PLIST="/Library/Google/Google Chrome Brand"
- SET_BRAND_FILE_ACCESS=yes
-fi
-# 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.
-BRAND_PATH="${BRAND_PATH_PLIST}.plist"
-if [ -n "${APP_BRAND}" ] ; then
- BRAND_PATH_DIR=$(dirname "${BRAND_PATH}")
- if [ ! -e "${BRAND_PATH_DIR}" ] ; then
- mkdir -p "${BRAND_PATH_DIR}"
- fi
- defaults write "${BRAND_PATH_PLIST}" "${BRAND_ID_KEY}" -string "${APP_BRAND}"
- if [ "${SET_BRAND_FILE_ACCESS}" = "yes" ] ; then
- chown "root:wheel" "${BRAND_PATH}" >& /dev/null
- chmod "a+r,u+w,go-w" "${BRAND_PATH}" >& /dev/null
+ # 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="$(defaults read "${installed_app_plist}" \
+ "${APP_VERSION_KEY}" || true)"
+ note "old_version_app = ${old_version_app}"
+
+ 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
-fi
-# Confirm that the brand file exists (it is optional)
-if [ ! -f "${BRAND_PATH}" ] ; then
- BRAND_PATH=
- # ksadmin reports an error if brand-path is cleared but brand-key still has a
- # value, so if there is no path, clear the key also.
- BRAND_ID_KEY=
-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="$(defaults read "${old_ks_plist}" \
+ "${KS_BRAND_KEY}" 2> /dev/null ||
+ true)"
+ note "old_brand = ${old_brand}"
+
+ # 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}"
-# Notify Keystone.
-if ksadmin_supports_brandpath_brandkey ; then
- ksadmin --register \
- -P "${PRODUCT_ID}" \
- --version "${NEW_VERSION_KS}" \
- --xcpath "${DEST}" \
- --url "${URL}" \
- --tag "${CHANNEL_ID}" \
- --tag-path "${DEST}/Contents/Info.plist" \
- --tag-key "${CHANNEL_ID_KEY}" \
- --brand-path "${BRAND_PATH}" \
- --brand-key "${BRAND_ID_KEY}" || exit 11
-elif ksadmin_supports_tagpath_tagkey ; then
- ksadmin --register \
- -P "${PRODUCT_ID}" \
- --version "${NEW_VERSION_KS}" \
- --xcpath "${DEST}" \
- --url "${URL}" \
- --tag "${CHANNEL_ID}" \
- --tag-path "${DEST}/Contents/Info.plist" \
- --tag-key "${CHANNEL_ID_KEY}" || exit 11
-elif ksadmin_supports_tag ; then
- ksadmin --register \
- -P "${PRODUCT_ID}" \
- --version "${NEW_VERSION_KS}" \
- --xcpath "${DEST}" \
- --url "${URL}" \
- --tag "${CHANNEL_ID}" || exit 11
-else
- ksadmin --register \
- -P "${PRODUCT_ID}" \
- --version "${NEW_VERSION_KS}" \
- --xcpath "${DEST}" \
- --url "${URL}" || exit 11
-fi
+ # 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.
+ #
+ # This fix-up is not necessary when running as root, because root will
+ # always be able to write everything needed.
+ #
+ # 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 whateer 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.
+ if [[ ${EUID} -ne 0 ]]; then
+ # This step isn't critical.
+ set +e
+ note "fixing installed symbolic links"
+
+ # Only consider symbolic links in ${update_app}. If there are any other
+ # links in ${installed_app} not present in ${update_app}, rsync will
+ # delete them as needed later. Use find -print0 with read -d $'\0' to
+ # handle even the weirdest paths.
+ local update_link
+ while IFS= read -r -d $'\0' update_link; do
+ # ${update_link} is relative to ${update_app}. Prepending
+ # ${installed_app} looks for the same link already on disk.
+ local installed_link="${installed_app}/${update_link}"
+ note "ensure_writable_symlink ${installed_link}"
+ ensure_writable_symlink "${installed_link}"
+ done < <(cd "${update_app}" && find . -type l -print0)
+
+ # Go back to how things were.
+ set -e
+ 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 verisoned 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 motiviations to additionally preserve the versioned
-# directory corresponding to the version that was just replaced.
-
-# Set the nullglob option. This causes a glob pattern that doesn't match
-# any files to expand to an empty string, instead of expanding to the glob
-# pattern itself. This means that if /path/* doesn't match anything, it will
-# expand to "" instead of, literally, "/path/*". The glob used in the loop
-# below is not expected to expand to nothing, but nullglob will prevent the
-# loop from trying to remove nonexistent directories by weird names with
-# funny characters in them.
-shopt -s nullglob
+ # 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. An rsync that
+ # excludes all contents is used to bring the permissions over from
+ # ${update_versions_dir}, otherwise, this directory would be the only one in
+ # the entire update exempt from getting its permissions copied over. A
+ # simple mkdir wouldn't copy mode bits. This is done even if
+ # ${installed_versions_dir} already does exist to ensure that the mode bits
+ # come from 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 CONTENTS_DIR"
+ if ! mkdir -p "${installed_app}/${CONTENTS_DIR}"; then
+ err "mkdir of CONTENTS_DIR failed"
+ exit 5
+ fi
+
+ local update_versions_dir="${update_app}/${VERSIONS_DIR}"
+ note "update_versions_dir = ${update_versions_dir}"
+
+ note "rsyncing VERSIONS_DIR"
+ if ! rsync ${RSYNC_FLAGS} --exclude "*" "${update_versions_dir}/" \
+ "${installed_versions_dir}"; then
+ err "rsync of VERSIONS_DIR failed, status ${PIPESTATUS[0]}"
+ exit 6
+ 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. Note that 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.
+ local update_versioned_dir new_versioned_dir
+ update_versioned_dir="${update_versions_dir}/${update_version_app}"
+ note "update_versioned_dir = ${update_versioned_dir}"
+ new_versioned_dir="${installed_versions_dir}/${update_version_app}"
+ note "new_versioned_dir = ${new_versioned_dir}"
+
+ 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
+
+ # 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 "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 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="$(defaults 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="$(defaults 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="$(defaults 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="$(defaults 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 cs_fwk="/System/Library/Frameworks/CoreServices.framework"
+ local ls_fwk="${cs_fwk}/Frameworks/LaunchServices.framework"
+ local lsregister="${ls_fwk}/Support/lsregister"
+ note "cs_fwk = ${cs_fwk}"
+ note "ls_fwk = ${ls_fwk}"
+ note "lsregister = ${lsregister}"
+ "${lsregister}" "${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}"
+
+ # 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.
+ local ksadmin_brand_plist_path="${brand_plist_path}"
+ local 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
+ note "ksadmin_brand_plist_path = ${ksadmin_brand_plist_path}"
+ note "ksadmin_brand_key = ${ksadmin_brand_key}"
+
+ note "notifying Keystone"
+
+ local ksadmin_args=(
+ --register
+ -P "${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
+
+ note "ksadmin_args = ${ksadmin_args[*]}"
-for versioned_dir in "${DEST}/Contents/Versions/"* ; do
- 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.
- 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.
- PS_STRING="${versioned_dir}/"
-
- # 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.
- LSOF_FILE="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}"
-
- # 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).
+ 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.
#
- # 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.
- rm -rf "${versioned_dir}"
- fi
-done
-
-# If this script is not running as root (indicating an update driven by a user
-# Keystone ticket) and 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 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 running as root, it's driven by a system Keystone ticket,
-# and future updates can be expected to be applied the same way, so
-# admin-writeability 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.
-CHMOD_MODE="a+rX,u+w,go-w"
-if [ ${EUID} -ne 0 ] ; then
- if [ "${DEST:0:14}" = "/Applications/" ] &&
- chgrp -Rh admin "${DEST}" >& /dev/null ; then
- CHMOD_MODE="a+rX,ug+w,o-w"
- fi
-else
- chown -Rh root:wheel "${DEST}" >& /dev/null
-fi
+ # 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 verisoned 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 motiviations 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
-chmod -R "${CHMOD_MODE}" "${DEST}" >& /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 "${DEST}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null
-
-# Host OS version check, to be able to take advantage of features on newer
-# systems and fall back to slow ways of doing things on older systems.
-OS_VERSION=$(sw_vers -productVersion)
-OS_MAJOR=$(sed -Ene 's/^([0-9]+).*/\1/p' <<< ${OS_VERSION})
-OS_MINOR=$(sed -Ene 's/^([0-9]+)\.([0-9]+).*/\2/p' <<< ${OS_VERSION})
-
-# 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.
-QUARANTINE_ATTR=com.apple.quarantine
-if [ ${OS_MAJOR} -gt 10 ] ||
- ([ ${OS_MAJOR} -eq 10 ] && [ ${OS_MINOR} -ge 6 ]) ; then
- # On 10.6, xattr supports -r for recursive operation.
- xattr -d -r "${QUARANTINE_ATTR}" "${DEST}" >& /dev/null
-else
- # On earlier systems, xattr doesn't support -r, so run xattr via find.
- find "${DEST}" -exec xattr -d "${QUARANTINE_ATTR}" {} + >& /dev/null
+ # 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
+
+ # Host OS version check, to be able to take advantage of features on newer
+ # systems and fall back to slow ways of doing things on older systems.
+ local os_version
+ os_version="$(sw_vers -productVersion)"
+ note "os_version = ${os_version}"
+
+ local os_major=0
+ local os_minor=0
+ if [[ "${os_version}" =~ ^([0-9]+)\.([0-9]+) ]]; then
+ os_major="${BASH_REMATCH[1]}"
+ os_minor="${BASH_REMATCH[2]}"
+ fi
+ note "os_major = ${os_major}"
+ note "os_minor = ${os_minor}"
+
+ # 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_major} -gt 10 ]] ||
+ ([[ ${os_major} -eq 10 ]] && [[ ${os_minor} -ge 6 ]]); 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
+
+ # Great success!
+ note "done!"
+ 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
-# Great success!
-exit 0
+main "${@}"
+exit ${?}