diff options
author | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-06-14 21:08:02 +0000 |
---|---|---|
committer | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-06-14 21:08:02 +0000 |
commit | 15b1c17f372dfe21fd5306ece4676a8c914b3e34 (patch) | |
tree | 3ee444983b9e4ad344eee8e4c7d2fb2ea3882b58 | |
parent | 14593524ba43e80db1c842df87026d0194ff823d (diff) | |
download | chromium_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-x | chrome/installer/mac/keystone_install.sh | 1392 |
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 ${?} |