| OLD | NEW |
| (Empty) |
| 1 #!/bin/bash | |
| 2 | |
| 3 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | |
| 4 # Use of this source code is governed by a BSD-style license that can be | |
| 5 # found in the LICENSE file. | |
| 6 | |
| 7 # Called by the Keystone system to update the installed application with a new | |
| 8 # version from a disk image. | |
| 9 | |
| 10 # Return values: | |
| 11 # 0 Happiness | |
| 12 # 1 Unknown failure | |
| 13 # 2 Basic sanity check source failure (e.g. no app on disk image) | |
| 14 # 3 Basic sanity check destination failure (e.g. ticket points to nothing) | |
| 15 # 4 Update driven by user ticket when a system ticket is also present | |
| 16 # 5 Could not prepare existing installed version to receive update | |
| 17 # 6 rsync failed (could not assure presence of Versions directory) | |
| 18 # 7 rsync failed (could not copy new versioned directory to Versions) | |
| 19 # 8 rsync failed (could not update outer .app bundle) | |
| 20 # 9 Could not get the version, update URL, or channel after update | |
| 21 # 10 Updated application does not have the version number from the update | |
| 22 # 11 ksadmin failure | |
| 23 | |
| 24 set -e | |
| 25 | |
| 26 # http://b/2290916: Keystone runs the installation with a restrictive PATH | |
| 27 # that only includes the directory containing ksadmin, /bin, and /usr/bin. It | |
| 28 # does not include /sbin or /usr/sbin. This script uses lsof, which is in | |
| 29 # /usr/sbin, and it's conceivable that it might want to use other tools in an | |
| 30 # sbin directory. Adjust the path accordingly. | |
| 31 export PATH="${PATH}:/sbin:/usr/sbin" | |
| 32 | |
| 33 # Returns 0 (true) if the parameter exists, is a symbolic link, and appears | |
| 34 # writable on the basis of its POSIX permissions. This is used to determine | |
| 35 # writeability like test's -w primary, but -w resolves symbolic links and this | |
| 36 # function does not. | |
| 37 function is_writable_symlink() { | |
| 38 SYMLINK=${1} | |
| 39 LINKMODE=$(stat -f %Sp "${SYMLINK}" 2> /dev/null || true) | |
| 40 if [ -z "${LINKMODE}" ] || [ "${LINKMODE:0:1}" != "l" ] ; then | |
| 41 return 1 | |
| 42 fi | |
| 43 LINKUSER=$(stat -f %u "${SYMLINK}" 2> /dev/null || true) | |
| 44 LINKGROUP=$(stat -f %g "${SYMLINK}" 2> /dev/null || true) | |
| 45 if [ -z "${LINKUSER}" ] || [ -z "${LINKGROUP}" ] ; then | |
| 46 return 1 | |
| 47 fi | |
| 48 | |
| 49 # If the users match, check the owner-write bit. | |
| 50 if [ ${EUID} -eq ${LINKUSER} ] ; then | |
| 51 if [ "${LINKMODE:2:1}" = "w" ] ; then | |
| 52 return 0 | |
| 53 fi | |
| 54 return 1 | |
| 55 fi | |
| 56 | |
| 57 # If the file's group matches any of the groups that this process is a | |
| 58 # member of, check the group-write bit. | |
| 59 GROUPMATCH= | |
| 60 for group in ${GROUPS[@]} ; do | |
| 61 if [ ${group} -eq ${LINKGROUP} ] ; then | |
| 62 GROUPMATCH=1 | |
| 63 break | |
| 64 fi | |
| 65 done | |
| 66 if [ -n "${GROUPMATCH}" ] ; then | |
| 67 if [ "${LINKMODE:5:1}" = "w" ] ; then | |
| 68 return 0 | |
| 69 fi | |
| 70 return 1 | |
| 71 fi | |
| 72 | |
| 73 # Check the other-write bit. | |
| 74 if [ "${LINKMODE:8:1}" = "w" ] ; then | |
| 75 return 0 | |
| 76 fi | |
| 77 return 1 | |
| 78 } | |
| 79 | |
| 80 # If SYMLINK exists and is a symbolic link, but is not writable according to | |
| 81 # is_writable_symlink, this function attempts to replace it with a new | |
| 82 # writable symbolic link. If FROM does not exist, is not a symbolic link, or | |
| 83 # is already writable, this function does nothing. This function always | |
| 84 # returns 0 (true). | |
| 85 function ensure_writable_symlink() { | |
| 86 SYMLINK=${1} | |
| 87 if [ -L "${SYMLINK}" ] && ! is_writable_symlink "${SYMLINK}" ; then | |
| 88 # If ${SYMLINK} refers to a directory, doing this naively might result in | |
| 89 # the new link being placed in that directory, instead of replacing the | |
| 90 # existing link. ln -fhs is supposed to handle this case, but it does so | |
| 91 # by unlinking (removing) the existing symbolic link before creating a new | |
| 92 # one. That leaves a small window during which the symbolic link is not | |
| 93 # present on disk at all. | |
| 94 # | |
| 95 # To avoid that possibility, a new symbolic link is created in a temporary | |
| 96 # location and then swapped into place with mv. An extra temporary | |
| 97 # directory is used to convince mv to replace the symbolic link: again, if | |
| 98 # the existing link refers to a directory, "mv newlink oldlink" will | |
| 99 # actually leave oldlink alone and place newlink into the directory. | |
| 100 # "mv newlink dirname(oldlink)" works as expected, but in order to replace | |
| 101 # oldlink, newlink must have the same basename, hence the temporary | |
| 102 # directory. | |
| 103 | |
| 104 TARGET=$(readlink "${SYMLINK}" 2> /dev/null || true) | |
| 105 if [ -z "${TARGET}" ] ; then | |
| 106 return 0 | |
| 107 fi | |
| 108 | |
| 109 SYMLINKDIR=$(dirname "${SYMLINK}") | |
| 110 TEMPLINKDIR="${SYMLINKDIR}/.symlink_temp.${$}.${RANDOM}" | |
| 111 TEMPLINK="${TEMPLINKDIR}/$(basename "${SYMLINK}")" | |
| 112 | |
| 113 # Don't bail out here if this fails. Something else will probably fail. | |
| 114 # Let it, it'll probably be easier to understand that failure than this | |
| 115 # one. | |
| 116 (mkdir "${TEMPLINKDIR}" && \ | |
| 117 ln -fhs "${TARGET}" "${TEMPLINK}" && \ | |
| 118 chmod -h 755 "${TEMPLINK}" && \ | |
| 119 mv -f "${TEMPLINK}" "${SYMLINKDIR}") || true | |
| 120 rm -rf "${TEMPLINKDIR}" | |
| 121 fi | |
| 122 | |
| 123 return 0 | |
| 124 } | |
| 125 | |
| 126 # Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to | |
| 127 # stdout. This function operates with "static" variables: it will only check | |
| 128 # the ksadmin version once per script run. If ksadmin is old enough to not | |
| 129 # support --ksadmin-version, or another error occurs, this function prints an | |
| 130 # empty string. | |
| 131 G_CHECKED_KSADMIN_VERSION= | |
| 132 G_KSADMIN_VERSION= | |
| 133 function ksadmin_version() { | |
| 134 if [ -z "${G_CHECKED_KSADMIN_VERSION}" ] ; then | |
| 135 G_CHECKED_KSADMIN_VERSION=1 | |
| 136 G_KSADMIN_VERSION=$(ksadmin --ksadmin-version || true) | |
| 137 fi | |
| 138 echo "${G_KSADMIN_VERSION}" | |
| 139 return 0 | |
| 140 } | |
| 141 | |
| 142 # Compares the installed ksadmin version against a supplied version number, | |
| 143 # and returns 0 (true) if the number to check is the same as or newer than the | |
| 144 # installed Keystone. Returns 1 (false) if the installed Keystone version | |
| 145 # number cannot be determined or if the number to check is less than the | |
| 146 # installed Keystone. The check argument should be a string of the form | |
| 147 # "major.minor.micro.build". | |
| 148 function is_ksadmin_version_ge() { | |
| 149 CHECK_VERSION=${1} | |
| 150 KSADMIN_VERSION=$(ksadmin_version) | |
| 151 if [ -n "${KSADMIN_VERSION}" ] ; then | |
| 152 VER_RE='^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$' | |
| 153 | |
| 154 KSADMIN_VERSION_MAJOR=$(sed -Ene "s/${VER_RE}/\1/p" <<< ${KSADMIN_VERSION}) | |
| 155 KSADMIN_VERSION_MINOR=$(sed -Ene "s/${VER_RE}/\2/p" <<< ${KSADMIN_VERSION}) | |
| 156 KSADMIN_VERSION_MICRO=$(sed -Ene "s/${VER_RE}/\3/p" <<< ${KSADMIN_VERSION}) | |
| 157 KSADMIN_VERSION_BUILD=$(sed -Ene "s/${VER_RE}/\4/p" <<< ${KSADMIN_VERSION}) | |
| 158 | |
| 159 CHECK_VERSION_MAJOR=$(sed -Ene "s/${VER_RE}/\1/p" <<< ${CHECK_VERSION}) | |
| 160 CHECK_VERSION_MINOR=$(sed -Ene "s/${VER_RE}/\2/p" <<< ${CHECK_VERSION}) | |
| 161 CHECK_VERSION_MICRO=$(sed -Ene "s/${VER_RE}/\3/p" <<< ${CHECK_VERSION}) | |
| 162 CHECK_VERSION_BUILD=$(sed -Ene "s/${VER_RE}/\4/p" <<< ${CHECK_VERSION}) | |
| 163 | |
| 164 if [ ${KSADMIN_VERSION_MAJOR} -gt ${CHECK_VERSION_MAJOR} ] || | |
| 165 ([ ${KSADMIN_VERSION_MAJOR} -eq ${CHECK_VERSION_MAJOR} ] && ( | |
| 166 [ ${KSADMIN_VERSION_MINOR} -gt ${CHECK_VERSION_MINOR} ] || | |
| 167 ([ ${KSADMIN_VERSION_MINOR} -eq ${CHECK_VERSION_MINOR} ] && ( | |
| 168 [ ${KSADMIN_VERSION_MICRO} -gt ${CHECK_VERSION_MICRO} ] || | |
| 169 ([ ${KSADMIN_VERSION_MICRO} -eq ${CHECK_VERSION_MICRO} ] && | |
| 170 [ ${KSADMIN_VERSION_BUILD} -ge ${CHECK_VERSION_BUILD} ]) | |
| 171 )) | |
| 172 )) ; then | |
| 173 return 0 | |
| 174 fi | |
| 175 fi | |
| 176 | |
| 177 return 1 | |
| 178 } | |
| 179 | |
| 180 # Returns 0 (true) if ksadmin supports --tag. | |
| 181 function ksadmin_supports_tag() { | |
| 182 KSADMIN_VERSION=$(ksadmin_version) | |
| 183 if [ -n "${KSADMIN_VERSION}" ] ; then | |
| 184 # A ksadmin that recognizes --ksadmin-version and provides a version | |
| 185 # number is new enough to recognize --tag. | |
| 186 return 0 | |
| 187 fi | |
| 188 return 1 | |
| 189 } | |
| 190 | |
| 191 # Returns 0 (true) if ksadmin supports --tag-path and --tag-key. | |
| 192 function ksadmin_supports_tagpath_tagkey() { | |
| 193 # --tag-path and --tag-key were introduced in Keystone 1.0.7.1306. | |
| 194 is_ksadmin_version_ge 1.0.7.1306 | |
| 195 # The return value of is_ksadmin_version_ge is used as this function's | |
| 196 # return value. | |
| 197 } | |
| 198 | |
| 199 # Returns 0 (true) if ksadmin supports --tag-path, --tag-key, --brand-path, | |
| 200 # and --brand-key. | |
| 201 function ksadmin_supports_brandpath_brandkey() { | |
| 202 # --brand-path and --brand-key were introduced in Keystone 1.0.8.1620. | |
| 203 # --tag-path and --tag-key are already supported if the brand arguments are | |
| 204 # also supported. | |
| 205 is_ksadmin_version_ge 1.0.8.1620 | |
| 206 # The return value of is_ksadmin_version_ge is used as this function's | |
| 207 # return value. | |
| 208 } | |
| 209 | |
| 210 # The argument should be the disk image path. Make sure it exists. | |
| 211 if [ $# -lt 1 ] || [ ! -d "${1}" ]; then | |
| 212 exit 2 | |
| 213 fi | |
| 214 | |
| 215 # Who we are. | |
| 216 PRODUCT_NAME="Google Chrome" | |
| 217 APP_DIR="${PRODUCT_NAME}.app" | |
| 218 FRAMEWORK_NAME="${PRODUCT_NAME} Framework" | |
| 219 FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" | |
| 220 SRC="${1}/${APP_DIR}" | |
| 221 | |
| 222 # Make sure that there's something to copy from, and that it's an absolute | |
| 223 # path. | |
| 224 if [ -z "${SRC}" ] || [ "${SRC:0:1}" != "/" ] || [ ! -d "${SRC}" ] ; then | |
| 225 exit 2 | |
| 226 fi | |
| 227 | |
| 228 # Figure out where we're going. Determine the application version to be | |
| 229 # installed, use that to locate the framework, and then look inside the | |
| 230 # framework for the Keystone product ID. | |
| 231 APP_VERSION_KEY="CFBundleShortVersionString" | |
| 232 UPD_VERSION_APP=$(defaults read "${SRC}/Contents/Info" "${APP_VERSION_KEY}" || | |
| 233 exit 2) | |
| 234 UPD_KS_PLIST="${SRC}/Contents/Versions/${UPD_VERSION_APP}/${FRAMEWORK_DIR}/Resou
rces/Info" | |
| 235 KS_VERSION_KEY="KSVersion" | |
| 236 UPD_VERSION_KS=$(defaults read "${UPD_KS_PLIST}" "${KS_VERSION_KEY}" || exit 2) | |
| 237 PRODUCT_ID=$(defaults read "${UPD_KS_PLIST}" KSProductID || exit 2) | |
| 238 if [ -z "${UPD_VERSION_KS}" ] || [ -z "${PRODUCT_ID}" ] ; then | |
| 239 exit 3 | |
| 240 fi | |
| 241 DEST=$(ksadmin -pP "${PRODUCT_ID}" | | |
| 242 sed -Ene \ | |
| 243 's%^[[:space:]]+xc=<KSPathExistenceChecker:.* path=(/.+)>$%\1%p') | |
| 244 | |
| 245 # More sanity checking. | |
| 246 if [ -z "${DEST}" ] || [ ! -d "${DEST}" ]; then | |
| 247 exit 3 | |
| 248 fi | |
| 249 | |
| 250 # If this script is not running as root, it's being driven by a user ticket. | |
| 251 # If a system ticket is also present, there's a potential for the two to | |
| 252 # collide. Both ticket types might be present if another user on the system | |
| 253 # promoted the ticket to system: the other user could not have removed this | |
| 254 # user's user ticket. Handle that case here by deleting the user ticket and | |
| 255 # exiting early with a discrete exit status. | |
| 256 # | |
| 257 # Current versions of ksadmin will exit 1 (false) when asked to print tickets | |
| 258 # and given a specific product ID to print. Older versions of ksadmin would | |
| 259 # exit 0 (true), but those same versions did not support -S (meaning to check | |
| 260 # the system ticket store) and would exit 1 (false) with this invocation due | |
| 261 # to not understanding the question. Therefore, the usage here will only | |
| 262 # delete the existing user ticket when running as non-root with access to a | |
| 263 # sufficiently recent ksadmin. Older ksadmins are tolerated: the update will | |
| 264 # likely fail for another reason and the user ticket will hang around until | |
| 265 # something is eventually able to remove it. | |
| 266 if [ ${EUID} -ne 0 ] && | |
| 267 ksadmin -S --print-tickets -P "${PRODUCT_ID}" >& /dev/null ; then | |
| 268 ksadmin --delete -P "${PRODUCT_ID}" || true | |
| 269 exit 4 | |
| 270 fi | |
| 271 | |
| 272 # Figure out what the existing version is using for its versioned directory. | |
| 273 # This will be used later, to avoid removing the currently-installed version's | |
| 274 # versioned directory in case anything is still using it. | |
| 275 OLD_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" || | |
| 276 true) | |
| 277 OLD_VERSIONED_DIR="${DEST}/Contents/Versions/${OLD_VERSION_APP}" | |
| 278 | |
| 279 # See if the timestamp of what's currently on disk is newer than the update's | |
| 280 # outer .app's timestamp. rsync will copy the update's timestamp over, but | |
| 281 # if that timestamp isn't as recent as what's already on disk, the .app will | |
| 282 # need to be touched. | |
| 283 NEEDS_TOUCH= | |
| 284 if [ "${DEST}" -nt "${SRC}" ] ; then | |
| 285 NEEDS_TOUCH=1 | |
| 286 fi | |
| 287 | |
| 288 # In some very weird and rare cases, it is possible to wind up with a user | |
| 289 # installation that contains symbolic links that the user does not have write | |
| 290 # permission over. More on how that might happen later. | |
| 291 # | |
| 292 # If a weird and rare case like this is observed, rsync will exit with an | |
| 293 # error when attempting to update the times on these symbolic links. rsync | |
| 294 # may not be intelligent enough to try creating a new symbolic link in these | |
| 295 # cases, but this script can be. | |
| 296 # | |
| 297 # This fix-up is not necessary when running as root, because root will always | |
| 298 # be able to write everything needed. | |
| 299 # | |
| 300 # The problem occurs when an administrative user first drag-installs the | |
| 301 # application to /Applications, resulting in the program's user being set to | |
| 302 # the user's own ID. If, subsequently, a .pkg package is installed over that, | |
| 303 # the existing directory ownership will be preserved, but file ownership will | |
| 304 # be changed to whateer is specified by the package, typically root. This | |
| 305 # applies to symbolic links as well. On a subsequent update, rsync will | |
| 306 # be able to copy the new files into place, because the user still has | |
| 307 # permission to write to the directories. If the symbolic link targets are | |
| 308 # not changing, though, rsync will not replace them, and they will remain | |
| 309 # owned by root. The user will not have permission to update the time on | |
| 310 # the symbolic links, resulting in an rsync error. | |
| 311 if [ ${EUID} -ne 0 ] ; then | |
| 312 # This step isn't critical. | |
| 313 set +e | |
| 314 | |
| 315 # Reset ${IFS} to deal with spaces in the for loop by not breaking the | |
| 316 # list up when they're encountered. | |
| 317 IFS_OLD="${IFS}" | |
| 318 IFS=$(printf '\n\t') | |
| 319 | |
| 320 # Only consider symbolic links in ${SRC}. If there are any other links in | |
| 321 # ${DEST} not present in ${SRC}, rsync will delete them as needed later. | |
| 322 LINKS=$(cd "${SRC}" && find . -type l) | |
| 323 | |
| 324 for link in ${LINKS} ; do | |
| 325 # ${link} is relative to ${SRC}. Prepending ${DEST} looks for the same | |
| 326 # link already on disk. | |
| 327 DESTLINK="${DEST}/${link}" | |
| 328 ensure_writable_symlink "${DESTLINK}" | |
| 329 done | |
| 330 | |
| 331 # Go back to how things were. | |
| 332 IFS="${IFS_OLD}" | |
| 333 set -e | |
| 334 fi | |
| 335 | |
| 336 # Collect the current app brand, it will be use later. | |
| 337 BRAND_ID_KEY=KSBrandID | |
| 338 APP_BRAND=$(defaults read "${DEST}/Contents/Info" "${BRAND_ID_KEY}" 2>/dev/null
|| | |
| 339 true) | |
| 340 | |
| 341 # Don't use rsync -a, because -a expands to -rlptgoD. -g and -o copy owners | |
| 342 # and groups, respectively, from the source, and that is undesirable in this | |
| 343 # case. -D copies devices and special files; copying devices only works | |
| 344 # when running as root, so for consistency between privileged and unprivileged | |
| 345 # operation, this option is omitted as well. | |
| 346 # -I, --ignore-times don't skip files that match in size and mod-time | |
| 347 # -l, --links copy symlinks as symlinks | |
| 348 # -r, --recursive recurse into directories | |
| 349 # -p, --perms preserve permissions | |
| 350 # -t, --times preserve times | |
| 351 RSYNC_FLAGS="-Ilprt" | |
| 352 | |
| 353 # By copying to ${DEST}, the existing application name will be preserved, even | |
| 354 # if the user has renamed the application on disk. Respecting the user's | |
| 355 # changes is friendly. | |
| 356 | |
| 357 # Make sure that the Versions directory exists, so that it can receive the | |
| 358 # versioned directory. It may not exist if updating from an older version | |
| 359 # that did not use the versioned layout on disk. An rsync that excludes all | |
| 360 # contents is used to bring the permissions over from the update's Versions | |
| 361 # directory, otherwise, this directory would be the only one in the entire | |
| 362 # update exempt from getting its permissions copied over. A simple mkdir | |
| 363 # wouldn't copy mode bits. This is done even if ${DEST}/Contents/Versions | |
| 364 # already does exist to ensure that the mode bits come from the update. | |
| 365 # | |
| 366 # ${DEST} is guaranteed to exist at this point, but ${DEST}/Contents may not | |
| 367 # if things are severely broken or if this update is actually an initial | |
| 368 # installation from a Keystone skeleton bootstrap. The mkdir creates | |
| 369 # ${DEST}/Contents if it doesn't exist; its mode bits will be fixed up in a | |
| 370 # subsequent rsync. | |
| 371 mkdir -p "${DEST}/Contents" || exit 5 | |
| 372 rsync ${RSYNC_FLAGS} --exclude "*" "${SRC}/Contents/Versions/" \ | |
| 373 "${DEST}/Contents/Versions" || exit 6 | |
| 374 | |
| 375 # Copy the versioned directory. The new versioned directory will have a | |
| 376 # different name than any existing one, so this won't harm anything already | |
| 377 # present in Contents/Versions, including the versioned directory being used | |
| 378 # by any running processes. If this step is interrupted, there will be an | |
| 379 # incomplete versioned directory left behind, but it won't interfere with | |
| 380 # anything, and it will be replaced or removed during a future update attempt. | |
| 381 NEW_VERSIONED_DIR="${DEST}/Contents/Versions/${UPD_VERSION_APP}" | |
| 382 rsync ${RSYNC_FLAGS} --delete-before \ | |
| 383 "${SRC}/Contents/Versions/${UPD_VERSION_APP}/" \ | |
| 384 "${NEW_VERSIONED_DIR}" || exit 7 | |
| 385 | |
| 386 # Copy the unversioned files into place, leaving everything in | |
| 387 # Contents/Versions alone. If this step is interrupted, the application will | |
| 388 # at least remain in a usable state, although it may not pass signature | |
| 389 # validation. Depending on when this step is interrupted, the application | |
| 390 # will either launch the old or the new version. The critical point is when | |
| 391 # the main executable is replaced. There isn't very much to copy in this step, | |
| 392 # because most of the application is in the versioned directory. This step | |
| 393 # only accounts for around 50 files, most of which are small localized | |
| 394 # InfoPlist.strings files. | |
| 395 rsync ${RSYNC_FLAGS} --delete-after --exclude /Contents/Versions \ | |
| 396 "${SRC}/" "${DEST}" || exit 8 | |
| 397 | |
| 398 # If necessary, touch the outermost .app so that it appears to the outside | |
| 399 # world that something was done to the bundle. This will cause LaunchServices | |
| 400 # to invalidate the information it has cached about the bundle even if | |
| 401 # lsregister does not run. This is not done if rsync already updated the | |
| 402 # timestamp to something newer than what had been on disk. This is not | |
| 403 # considered a critical step, and if it fails, this script will not exit. | |
| 404 if [ -n "${NEEDS_TOUCH}" ] ; then | |
| 405 touch -cf "${DEST}" || true | |
| 406 fi | |
| 407 | |
| 408 # Read the new values (e.g. version). Get the installed application version | |
| 409 # to get the path to the framework, where the Keystone keys are stored. | |
| 410 NEW_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" || | |
| 411 exit 9) | |
| 412 NEW_KS_PLIST="${DEST}/Contents/Versions/${NEW_VERSION_APP}/${FRAMEWORK_DIR}/Reso
urces/Info" | |
| 413 NEW_VERSION_KS=$(defaults read "${NEW_KS_PLIST}" "${KS_VERSION_KEY}" || exit 9) | |
| 414 URL=$(defaults read "${NEW_KS_PLIST}" KSUpdateURL || exit 9) | |
| 415 # The channel ID is optional. Suppress stderr to prevent Keystone from seeing | |
| 416 # possible error output. | |
| 417 CHANNEL_ID_KEY=KSChannelID | |
| 418 CHANNEL_ID=$(defaults read "${NEW_KS_PLIST}" "${CHANNEL_ID_KEY}" 2>/dev/null || | |
| 419 true) | |
| 420 | |
| 421 # Make sure that the update was successful by comparing the version found in | |
| 422 # the update with the version now on disk. | |
| 423 if [ "${NEW_VERSION_KS}" != "${UPD_VERSION_KS}" ]; then | |
| 424 exit 10 | |
| 425 fi | |
| 426 | |
| 427 # Notify LaunchServices. This is not considered a critical step, and | |
| 428 # lsregister's exit codes shouldn't be confused with this script's own. | |
| 429 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.fram
ework/Support/lsregister "${DEST}" || true | |
| 430 | |
| 431 # Call ksadmin_version once to prime the global state. This is needed because | |
| 432 # subsequent calls to ksadmin_version that occur in $(...) expansions will not | |
| 433 # affect the global state (although they can read from the already-initialized | |
| 434 # global state) and thus will cause a new ksadmin --ksadmin-version process to | |
| 435 # run for each check unless the globals have been properly initialized | |
| 436 # beforehand. | |
| 437 ksadmin_version >& /dev/null || true | |
| 438 | |
| 439 # The brand information is stored differently depending on whether this is | |
| 440 # running for a system or user ticket. | |
| 441 BRAND_PATH_PLIST= | |
| 442 SET_BRAND_FILE_ACCESS=no | |
| 443 if [ ${EUID} -ne 0 ] ; then | |
| 444 # Using a user level ticket. | |
| 445 BRAND_PATH_PLIST=~/"Library/Google/Google Chrome Brand" | |
| 446 else | |
| 447 # Using a system level ticket. | |
| 448 BRAND_PATH_PLIST="/Library/Google/Google Chrome Brand" | |
| 449 SET_BRAND_FILE_ACCESS=yes | |
| 450 fi | |
| 451 # If the user manually updated their copy of Chrome, there might be new brand | |
| 452 # information in the app bundle, and that needs to be copied out into the | |
| 453 # file Keystone looks at. | |
| 454 BRAND_PATH="${BRAND_PATH_PLIST}.plist" | |
| 455 if [ -n "${APP_BRAND}" ] ; then | |
| 456 BRAND_PATH_DIR=$(dirname "${BRAND_PATH}") | |
| 457 if [ ! -e "${BRAND_PATH_DIR}" ] ; then | |
| 458 mkdir -p "${BRAND_PATH_DIR}" | |
| 459 fi | |
| 460 defaults write "${BRAND_PATH_PLIST}" "${BRAND_ID_KEY}" -string "${APP_BRAND}" | |
| 461 if [ "${SET_BRAND_FILE_ACCESS}" = "yes" ] ; then | |
| 462 chown "root:wheel" "${BRAND_PATH}" >& /dev/null | |
| 463 chmod "a+r,u+w,go-w" "${BRAND_PATH}" >& /dev/null | |
| 464 fi | |
| 465 fi | |
| 466 # Confirm that the brand file exists (it is optional) | |
| 467 if [ ! -f "${BRAND_PATH}" ] ; then | |
| 468 BRAND_PATH= | |
| 469 # ksadmin reports an error if brand-path is cleared but brand-key still has a | |
| 470 # value, so if there is no path, clear the key also. | |
| 471 BRAND_ID_KEY= | |
| 472 fi | |
| 473 | |
| 474 # Notify Keystone. | |
| 475 if ksadmin_supports_brandpath_brandkey ; then | |
| 476 ksadmin --register \ | |
| 477 -P "${PRODUCT_ID}" \ | |
| 478 --version "${NEW_VERSION_KS}" \ | |
| 479 --xcpath "${DEST}" \ | |
| 480 --url "${URL}" \ | |
| 481 --tag "${CHANNEL_ID}" \ | |
| 482 --tag-path "${DEST}/Contents/Info.plist" \ | |
| 483 --tag-key "${CHANNEL_ID_KEY}" \ | |
| 484 --brand-path "${BRAND_PATH}" \ | |
| 485 --brand-key "${BRAND_ID_KEY}" || exit 11 | |
| 486 elif ksadmin_supports_tagpath_tagkey ; then | |
| 487 ksadmin --register \ | |
| 488 -P "${PRODUCT_ID}" \ | |
| 489 --version "${NEW_VERSION_KS}" \ | |
| 490 --xcpath "${DEST}" \ | |
| 491 --url "${URL}" \ | |
| 492 --tag "${CHANNEL_ID}" \ | |
| 493 --tag-path "${DEST}/Contents/Info.plist" \ | |
| 494 --tag-key "${CHANNEL_ID_KEY}" || exit 11 | |
| 495 elif ksadmin_supports_tag ; then | |
| 496 ksadmin --register \ | |
| 497 -P "${PRODUCT_ID}" \ | |
| 498 --version "${NEW_VERSION_KS}" \ | |
| 499 --xcpath "${DEST}" \ | |
| 500 --url "${URL}" \ | |
| 501 --tag "${CHANNEL_ID}" || exit 11 | |
| 502 else | |
| 503 ksadmin --register \ | |
| 504 -P "${PRODUCT_ID}" \ | |
| 505 --version "${NEW_VERSION_KS}" \ | |
| 506 --xcpath "${DEST}" \ | |
| 507 --url "${URL}" || exit 11 | |
| 508 fi | |
| 509 | |
| 510 # The remaining steps are not considered critical. | |
| 511 set +e | |
| 512 | |
| 513 # Try to clean up old versions that are not in use. The strategy is to keep | |
| 514 # the versioned directory corresponding to the update just applied | |
| 515 # (obviously) and the version that was just replaced, and to use ps and lsof | |
| 516 # to see if it looks like any processes are currently using any other old | |
| 517 # directories. Directories not in use are removed. Old versioned directories | |
| 518 # that are in use are left alone so as to not interfere with running | |
| 519 # processes. These directories can be cleaned up by this script on future | |
| 520 # updates. | |
| 521 # | |
| 522 # To determine which directories are in use, both ps and lsof are used. Each | |
| 523 # approach has limitations. | |
| 524 # | |
| 525 # The ps check looks for processes within the verisoned directory. Only | |
| 526 # helper processes, such as renderers, are within the versioned directory. | |
| 527 # Browser processes are not, so the ps check will not find them, and will | |
| 528 # assume that a versioned directory is not in use if a browser is open without | |
| 529 # any windows. The ps mechanism can also only detect processes running on the | |
| 530 # system that is performing the update. If network shares are involved, all | |
| 531 # bets are off. | |
| 532 # | |
| 533 # The lsof check looks to see what processes have the framework dylib open. | |
| 534 # Browser processes will have their versioned framework dylib open, so this | |
| 535 # check is able to catch browsers even if there are no associated helper | |
| 536 # processes. Like the ps check, the lsof check is limited to processes on | |
| 537 # the system that is performing the update. Finally, unless running as root, | |
| 538 # the lsof check can only find processes running as the effective user | |
| 539 # performing the update. | |
| 540 # | |
| 541 # These limitations are motiviations to additionally preserve the versioned | |
| 542 # directory corresponding to the version that was just replaced. | |
| 543 | |
| 544 # Set the nullglob option. This causes a glob pattern that doesn't match | |
| 545 # any files to expand to an empty string, instead of expanding to the glob | |
| 546 # pattern itself. This means that if /path/* doesn't match anything, it will | |
| 547 # expand to "" instead of, literally, "/path/*". The glob used in the loop | |
| 548 # below is not expected to expand to nothing, but nullglob will prevent the | |
| 549 # loop from trying to remove nonexistent directories by weird names with | |
| 550 # funny characters in them. | |
| 551 shopt -s nullglob | |
| 552 | |
| 553 for versioned_dir in "${DEST}/Contents/Versions/"* ; do | |
| 554 if [ "${versioned_dir}" = "${NEW_VERSIONED_DIR}" ] || \ | |
| 555 [ "${versioned_dir}" = "${OLD_VERSIONED_DIR}" ] ; then | |
| 556 # This is the versioned directory corresponding to the update that was | |
| 557 # just applied or the version that was previously in use. Leave it alone. | |
| 558 continue | |
| 559 fi | |
| 560 | |
| 561 # Look for any processes whose executables are within this versioned | |
| 562 # directory. They'll be helper processes, such as renderers. Their | |
| 563 # existence indicates that this versioned directory is currently in use. | |
| 564 PS_STRING="${versioned_dir}/" | |
| 565 | |
| 566 # Look for any processes using the framework dylib. This will catch | |
| 567 # browser processes where the ps check will not, but it is limited to | |
| 568 # processes running as the effective user. | |
| 569 LSOF_FILE="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}" | |
| 570 | |
| 571 # ps -e displays all users' processes, -ww causes ps to not truncate lines, | |
| 572 # -o comm instructs it to only print the command name, and the = tells it to | |
| 573 # not print a header line. | |
| 574 # The cut invocation filters the ps output to only have at most the number | |
| 575 # of characters in ${PS_STRING}. This is done so that grep can look for an | |
| 576 # exact match. | |
| 577 # grep -F tells grep to look for lines that are exact matches (not regular | |
| 578 # expressions), -q tells it to not print any output and just indicate | |
| 579 # matches by exit status, and -x tells it that the entire line must match | |
| 580 # ${PS_STRING} exactly, as opposed to matching a substring. A match | |
| 581 # causes grep to exit zero (true). | |
| 582 # | |
| 583 # lsof will exit nonzero if ${LSOF_FILE} does not exist or is open by any | |
| 584 # process. If the file exists and is open, it will exit zero (true). | |
| 585 if (! ps -ewwo comm= | \ | |
| 586 cut -c "1-${#PS_STRING}" | \ | |
| 587 grep -Fqx "${PS_STRING}") && | |
| 588 (! lsof "${LSOF_FILE}" >& /dev/null) ; then | |
| 589 # It doesn't look like anything is using this versioned directory. Get rid | |
| 590 # of it. | |
| 591 rm -rf "${versioned_dir}" | |
| 592 fi | |
| 593 done | |
| 594 | |
| 595 # If this script is not running as root (indicating an update driven by a user | |
| 596 # Keystone ticket) and the application is installed somewhere under | |
| 597 # /Applications, try to make it writable by all admin users. This will allow | |
| 598 # other admin users to update the application from their own user Keystone | |
| 599 # instances. | |
| 600 # | |
| 601 # If the script is not running as root and the application is not installed | |
| 602 # under /Applications, it might not be in a system-wide location, and it | |
| 603 # probably won't be something that other users on the system are running, so | |
| 604 # err on the side of safety and don't make it group-writable. | |
| 605 # | |
| 606 # If this script is running as root, it's driven by a system Keystone ticket, | |
| 607 # and future updates can be expected to be applied the same way, so | |
| 608 # admin-writeability is not a concern. Set the entire thing to be owned by | |
| 609 # root in that case, regardless of where it's installed, and drop any group | |
| 610 # and other write permission. | |
| 611 # | |
| 612 # If this script is running as a user that is not a member of the admin group, | |
| 613 # the chgrp operation will not succeed. Tolerate that case, because it's | |
| 614 # better than the alternative, which is to make the application | |
| 615 # world-writable. | |
| 616 CHMOD_MODE="a+rX,u+w,go-w" | |
| 617 if [ ${EUID} -ne 0 ] ; then | |
| 618 if [ "${DEST:0:14}" = "/Applications/" ] && | |
| 619 chgrp -Rh admin "${DEST}" >& /dev/null ; then | |
| 620 CHMOD_MODE="a+rX,ug+w,o-w" | |
| 621 fi | |
| 622 else | |
| 623 chown -Rh root:wheel "${DEST}" >& /dev/null | |
| 624 fi | |
| 625 | |
| 626 chmod -R "${CHMOD_MODE}" "${DEST}" >& /dev/null | |
| 627 | |
| 628 # On the Mac, or at least on HFS+, symbolic link permissions are significant, | |
| 629 # but chmod -R and -h can't be used together. Do another pass to fix the | |
| 630 # permissions on any symbolic links. | |
| 631 find "${DEST}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null | |
| 632 | |
| 633 # Host OS version check, to be able to take advantage of features on newer | |
| 634 # systems and fall back to slow ways of doing things on older systems. | |
| 635 OS_VERSION=$(sw_vers -productVersion) | |
| 636 OS_MAJOR=$(sed -Ene 's/^([0-9]+).*/\1/p' <<< ${OS_VERSION}) | |
| 637 OS_MINOR=$(sed -Ene 's/^([0-9]+)\.([0-9]+).*/\2/p' <<< ${OS_VERSION}) | |
| 638 | |
| 639 # If an update is triggered from within the application itself, the update | |
| 640 # process inherits the quarantine bit (LSFileQuarantineEnabled). Any files or | |
| 641 # directories created during the update will be quarantined in that case, | |
| 642 # which may cause Launch Services to display quarantine UI. That's bad, | |
| 643 # especially if it happens when the outer .app launches a quarantined inner | |
| 644 # helper. If the application is already on the system and is being updated, | |
| 645 # then it can be assumed that it should not be quarantined. Use xattr to drop | |
| 646 # the quarantine attribute. | |
| 647 # | |
| 648 # TODO(mark): Instead of letting the quarantine attribute be set and then | |
| 649 # dropping it here, figure out a way to get the update process to run without | |
| 650 # LSFileQuarantineEnabled even when triggering an update from within the | |
| 651 # application. | |
| 652 QUARANTINE_ATTR=com.apple.quarantine | |
| 653 if [ ${OS_MAJOR} -gt 10 ] || | |
| 654 ([ ${OS_MAJOR} -eq 10 ] && [ ${OS_MINOR} -ge 6 ]) ; then | |
| 655 # On 10.6, xattr supports -r for recursive operation. | |
| 656 xattr -d -r "${QUARANTINE_ATTR}" "${DEST}" >& /dev/null | |
| 657 else | |
| 658 # On earlier systems, xattr doesn't support -r, so run xattr via find. | |
| 659 find "${DEST}" -exec xattr -d "${QUARANTINE_ATTR}" {} + >& /dev/null | |
| 660 fi | |
| 661 | |
| 662 # Great success! | |
| 663 exit 0 | |
| OLD | NEW |