1 #!/bin/bash -p 2 3 # Copyright (c) 2012 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 # usage: keystone_install.sh update_dmg_mount_point 8 # 9 # Called by the Keystone system to update the installed application with a new 10 # version from a disk image. 11 # 12 # Environment variables: 13 # GOOGLE_CHROME_UPDATER_DEBUG 14 # When set to a non-empty value, additional information about this script's 15 # actions will be logged to stderr. The same debugging information will 16 # also be enabled when "Library/Google/Google Chrome Updater Debug" in the 17 # root directory or in ${HOME} exists. 18 # 19 # Exit codes: 20 # 0 Happiness 21 # 1 Unknown failure 22 # 2 Basic sanity check source failure (e.g. no app on disk image) 23 # 3 Basic sanity check destination failure (e.g. ticket points to nothing) 24 # 4 Update driven by user ticket when a system ticket is also present 25 # 5 Could not prepare existing installed version to receive update 26 # 6 Patch sanity check failure 27 # 7 rsync failed (could not copy new versioned directory to Versions) 28 # 8 rsync failed (could not update outer .app bundle) 29 # 9 Could not get the version, update URL, or channel after update 30 # 10 Updated application does not have the version number from the update 31 # 11 ksadmin failure 32 # 12 dirpatcher failed for versioned directory 33 # 13 dirpatcher failed for outer .app bundle 34 # 35 # The following exit codes are not used by this script, but can be used to 36 # convey special meaning to Keystone: 37 # 66 (unused) success, request reboot 38 # 77 (unused) try installation again later 39 40 set -eu 41 42 # http://b/2290916: Keystone runs the installation with a restrictive PATH 43 # that only includes the directory containing ksadmin, /bin, and /usr/bin. It 44 # does not include /sbin or /usr/sbin. This script uses lsof, which is in 45 # /usr/sbin, and it's conceivable that it might want to use other tools in an 46 # sbin directory. Adjust the path accordingly. 47 export PATH="${PATH}:/sbin:/usr/sbin" 48 49 # Environment sanitization. Clear environment variables that might impact the 50 # interpreter's operation. The |bash -p| invocation on the #! line takes the 51 # bite out of BASH_ENV, ENV, and SHELLOPTS (among other features), but 52 # clearing them here ensures that they won't impact any shell scripts used as 53 # utility programs. SHELLOPTS is read-only and can't be unset, only 54 # unexported. 55 unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT 56 export -n SHELLOPTS 57 58 set -o pipefail 59 shopt -s nullglob 60 61 ME="$(basename "${0}")" 62 readonly ME 63 64 # Workaround for http://code.google.com/p/chromium/issues/detail?id=83180#c3 65 # In bash 4.0, "declare VAR" no longer initializes VAR if not already set. 66 : ${GOOGLE_CHROME_UPDATER_DEBUG:=} 67 err() { 68 local error="${1}" 69 70 local id= 71 if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then 72 id=": ${$} $(date "+%Y-%m-%d %H:%M:%S %z")" 73 fi 74 75 echo "${ME}${id}: ${error}" >& 2 76 } 77 78 note() { 79 local message="${1}" 80 81 if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then 82 err "${message}" 83 fi 84 } 85 86 g_temp_dir= 87 cleanup() { 88 local status=${?} 89 90 trap - EXIT 91 trap '' HUP INT QUIT TERM 92 93 if [[ ${status} -ge 128 ]]; then 94 err "Caught signal $((${status} - 128))" 95 fi 96 97 if [[ -n "${g_temp_dir}" ]]; then 98 rm -rf "${g_temp_dir}" 99 fi 100 101 exit ${status} 102 } 103 104 ensure_temp_dir() { 105 if [[ -z "${g_temp_dir}" ]]; then 106 # Choose a template that won't be a dot directory. Make it safe by 107 # removing leading hyphens, too. 108 local template="${ME}" 109 if [[ "${template}" =~ ^[-.]+(.*)$ ]]; then 110 template="${BASH_REMATCH[1]}" 111 fi 112 if [[ -z "${template}" ]]; then 113 template="keystone_install" 114 fi 115 116 g_temp_dir="$(mktemp -d -t "${template}")" 117 note "g_temp_dir = ${g_temp_dir}" 118 fi 119 } 120 121 # Returns 0 (true) if |symlink| exists, is a symbolic link, and appears 122 # writable on the basis of its POSIX permissions. This is used to determine 123 # writability like test's -w primary, but -w resolves symbolic links and this 124 # function does not. 125 is_writable_symlink() { 126 local symlink="${1}" 127 128 local link_mode 129 link_mode="$(stat -f %Sp "${symlink}" 2> /dev/null || true)" 130 if [[ -z "${link_mode}" ]] || [[ "${link_mode:0:1}" != "l" ]]; then 131 return 1 132 fi 133 134 local link_user link_group 135 link_user="$(stat -f %u "${symlink}" 2> /dev/null || true)" 136 link_group="$(stat -f %g "${symlink}" 2> /dev/null || true)" 137 if [[ -z "${link_user}" ]] || [[ -z "${link_group}" ]]; then 138 return 1 139 fi 140 141 # If the users match, check the owner-write bit. 142 if [[ ${EUID} -eq "${link_user}" ]]; then 143 if [[ "${link_mode:2:1}" = "w" ]]; then 144 return 0 145 fi 146 return 1 147 fi 148 149 # If the file's group matches any of the groups that this process is a 150 # member of, check the group-write bit. 151 local group_match= 152 local group 153 for group in "${GROUPS[@]}"; do 154 if [[ "${group}" -eq "${link_group}" ]]; then 155 group_match="y" 156 break 157 fi 158 done 159 if [[ -n "${group_match}" ]]; then 160 if [[ "${link_mode:5:1}" = "w" ]]; then 161 return 0 162 fi 163 return 1 164 fi 165 166 # Check the other-write bit. 167 if [[ "${link_mode:8:1}" = "w" ]]; then 168 return 0 169 fi 170 171 return 1 172 } 173 174 # If |symlink| exists and is a symbolic link, but is not writable according to 175 # is_writable_symlink, this function attempts to replace it with a new 176 # writable symbolic link. If |symlink| does not exist, is not a symbolic 177 # link, or is already writable, this function does nothing. This function 178 # always returns 0 (true). 179 ensure_writable_symlink() { 180 local symlink="${1}" 181 182 if [[ -L "${symlink}" ]] && ! is_writable_symlink "${symlink}"; then 183 # If ${symlink} refers to a directory, doing this naively might result in 184 # the new link being placed in that directory, instead of replacing the 185 # existing link. ln -fhs is supposed to handle this case, but it does so 186 # by unlinking (removing) the existing symbolic link before creating a new 187 # one. That leaves a small window during which the symbolic link is not 188 # present on disk at all. 189 # 190 # To avoid that possibility, a new symbolic link is created in a temporary 191 # location and then swapped into place with mv. An extra temporary 192 # directory is used to convince mv to replace the symbolic link: again, if 193 # the existing link refers to a directory, "mv newlink oldlink" will 194 # actually leave oldlink alone and place newlink into the directory. 195 # "mv newlink dirname(oldlink)" works as expected, but in order to replace 196 # oldlink, newlink must have the same basename, hence the temporary 197 # directory. 198 199 local target 200 target="$(readlink "${symlink}" 2> /dev/null || true)" 201 if [[ -z "${target}" ]]; then 202 return 0 203 fi 204 205 # Error handling strategy: if anything fails, such as the mktemp, ln, 206 # chmod, or mv, ignore the failure and return 0 (success), leaving the 207 # existing state with the non-writable symbolic link intact. Failures 208 # in this function will be difficult to understand and diagnose, and a 209 # non-writable symbolic link is not necessarily fatal. If something else 210 # requires a writable symbolic link, allowing it to fail when a symbolic 211 # link is not writable is easier to understand than bailing out of the 212 # script on failure here. 213 214 local symlink_dir temp_link_dir temp_link 215 symlink_dir="$(dirname "${symlink}")" 216 temp_link_dir="$(mktemp -d "${symlink_dir}/.symlink_temp.XXXXXX" || true)" 217 if [[ -z "${temp_link_dir}" ]]; then 218 return 0 219 fi 220 temp_link="${temp_link_dir}/$(basename "${symlink}")" 221 222 (ln -fhs "${target}" "${temp_link}" && \ 223 chmod -h 755 "${temp_link}" && \ 224 mv -f "${temp_link}" "${symlink_dir}/") || true 225 rm -rf "${temp_link_dir}" 226 fi 227 228 return 0 229 } 230 231 # ensure_writable_symlinks_recursive calls ensure_writable_symlink for every 232 # symbolic link in |directory|, recursively. 233 # 234 # In some very weird and rare cases, it is possible to wind up with a user 235 # installation that contains symbolic links that the user does not have write 236 # permission over. More on how that might happen later. 237 # 238 # If a weird and rare case like this is observed, rsync will exit with an 239 # error when attempting to update the times on these symbolic links. rsync 240 # may not be intelligent enough to try creating a new symbolic link in these 241 # cases, but this script can be. 242 # 243 # The problem occurs when an administrative user first drag-installs the 244 # application to /Applications, resulting in the program's user being set to 245 # the user's own ID. If, subsequently, a .pkg package is installed over that, 246 # the existing directory ownership will be preserved, but file ownership will 247 # be changed to whatever is specified by the package, typically root. This 248 # applies to symbolic links as well. On a subsequent update, rsync will be 249 # able to copy the new files into place, because the user still has permission 250 # to write to the directories. If the symbolic link targets are not changing, 251 # though, rsync will not replace them, and they will remain owned by root. 252 # The user will not have permission to update the time on the symbolic links, 253 # resulting in an rsync error. 254 ensure_writable_symlinks_recursive() { 255 local directory="${1}" 256 257 # This fix-up is not necessary when running as root, because root will 258 # always be able to write everything needed. 259 if [[ ${EUID} -eq 0 ]]; then 260 return 0 261 fi 262 263 # This step isn't critical. 264 local set_e= 265 if [[ "${-}" =~ e ]]; then 266 set_e="y" 267 set +e 268 fi 269 270 # Use find -print0 with read -d $'\0' to handle even the weirdest paths. 271 local symlink 272 while IFS= read -r -d $'\0' symlink; do 273 ensure_writable_symlink "${symlink}" 274 done < <(find "${directory}" -type l -print0) 275 276 # Go back to how things were. 277 if [[ -n "${set_e}" ]]; then 278 set -e 279 fi 280 } 281 282 # is_version_ge accepts two version numbers, left and right, and performs a 283 # piecewise comparison determining the result of left >= right, returning true 284 # (0) if left >= right, and false (1) if left < right. If left or right are 285 # missing components relative to the other, the missing components are assumed 286 # to be 0, such that 10.6 == 10.6.0. 287 is_version_ge() { 288 local left="${1}" 289 local right="${2}" 290 291 local -a left_array right_array 292 IFS=. left_array=(${left}) 293 IFS=. right_array=(${right}) 294 295 local left_count=${#left_array[@]} 296 local right_count=${#right_array[@]} 297 local count=${left_count} 298 if [[ ${right_count} -lt ${count} ]]; then 299 count=${right_count} 300 fi 301 302 # Compare the components piecewise, as long as there are corresponding 303 # components on each side. If left_element and right_element are unequal, 304 # a comparison can be made. 305 local index=0 306 while [[ ${index} -lt ${count} ]]; do 307 local left_element="${left_array[${index}]}" 308 local right_element="${right_array[${index}]}" 309 if [[ ${left_element} -gt ${right_element} ]]; then 310 return 0 311 elif [[ ${left_element} -lt ${right_element} ]]; then 312 return 1 313 fi 314 ((++index)) 315 done 316 317 # If there are more components on the left than on the right, continue 318 # comparing, assuming 0 for each of the missing components on the right. 319 while [[ ${index} -lt ${left_count} ]]; do 320 local left_element="${left_array[${index}]}" 321 if [[ ${left_element} -gt 0 ]]; then 322 return 0 323 fi 324 ((++index)) 325 done 326 327 # If there are more components on the right than on the left, continue 328 # comparing, assuming 0 for each of the missing components on the left. 329 while [[ ${index} -lt ${right_count} ]]; do 330 local right_element="${right_array[${index}]}" 331 if [[ ${right_element} -gt 0 ]]; then 332 return 1 333 fi 334 ((++index)) 335 done 336 337 # Upon reaching this point, the two version numbers are semantically equal. 338 return 0 339 } 340 341 # Prints the OS version, as reported by sw_vers -productVersion, to stdout. 342 # This function operates with "static" variables: it will only check the OS 343 # version once per script run. 344 g_checked_os_version= 345 g_os_version= 346 os_version() { 347 if [[ -z "${g_checked_os_version}" ]]; then 348 g_checked_os_version="y" 349 g_os_version="$(sw_vers -productVersion)" 350 note "g_os_version = ${g_os_version}" 351 fi 352 echo "${g_os_version}" 353 return 0 354 } 355 356 # Compares the running OS version against a supplied version number, 357 # |check_version|, and returns 0 (true) if the running OS version is greater 358 # than or equal to |check_version| according to a piece-wise comparison. 359 # Returns 1 (false) if the running OS version number cannot be determined or 360 # if |check_version| is greater than the running OS version. |check_version| 361 # should be a string of the form "major.minor" or "major.minor.micro". 362 is_os_version_ge() { 363 local check_version="${1}" 364 365 local os_version="$(os_version)" 366 is_version_ge "${os_version}" "${check_version}" 367 368 # The return value of is_version_ge is used as this function's return value. 369 } 370 371 # Returns 0 (true) if xattr supports -r for recursive operation. 372 os_xattr_supports_r() { 373 # xattr -r is supported in Mac OS X 10.6. 374 is_os_version_ge 10.6 375 376 # The return value of is_os_version_ge is used as this function's return 377 # value. 378 } 379 380 # Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to 381 # stdout. This function operates with "static" variables: it will only check 382 # the ksadmin version once per script run. If ksadmin is old enough to not 383 # support --ksadmin-version, or another error occurs, this function prints an 384 # empty string. 385 g_checked_ksadmin_version= 386 g_ksadmin_version= 387 ksadmin_version() { 388 if [[ -z "${g_checked_ksadmin_version}" ]]; then 389 g_checked_ksadmin_version="y" 390 g_ksadmin_version="$(ksadmin --ksadmin-version || true)" 391 note "g_ksadmin_version = ${g_ksadmin_version}" 392 fi 393 echo "${g_ksadmin_version}" 394 return 0 395 } 396 397 # Compares the installed ksadmin version against a supplied version number, 398 # |check_version|, and returns 0 (true) if the installed Keystone version is 399 # greater than or equal to |check_version| according to a piece-wise 400 # comparison. Returns 1 (false) if the installed Keystone version number 401 # cannot be determined or if |check_version| is greater than the installed 402 # Keystone version. |check_version| should be a string of the form 403 # "major.minor.micro.build". 404 is_ksadmin_version_ge() { 405 local check_version="${1}" 406 407 local ksadmin_version="$(ksadmin_version)" 408 is_version_ge "${ksadmin_version}" "${check_version}" 409 410 # The return value of is_version_ge is used as this function's return value. 411 } 412 413 # Returns 0 (true) if ksadmin supports --tag. 414 ksadmin_supports_tag() { 415 local ksadmin_version 416 417 ksadmin_version="$(ksadmin_version)" 418 if [[ -n "${ksadmin_version}" ]]; then 419 # A ksadmin that recognizes --ksadmin-version and provides a version 420 # number is new enough to recognize --tag. 421 return 0 422 fi 423 424 return 1 425 } 426 427 # Returns 0 (true) if ksadmin supports --tag-path and --tag-key. 428 ksadmin_supports_tagpath_tagkey() { 429 # --tag-path and --tag-key were introduced in Keystone 1.0.7.1306. 430 is_ksadmin_version_ge 1.0.7.1306 431 432 # The return value of is_ksadmin_version_ge is used as this function's 433 # return value. 434 } 435 436 # Returns 0 (true) if ksadmin supports --brand-path and --brand-key. 437 ksadmin_supports_brandpath_brandkey() { 438 # --brand-path and --brand-key were introduced in Keystone 1.0.8.1620. 439 is_ksadmin_version_ge 1.0.8.1620 440 441 # The return value of is_ksadmin_version_ge is used as this function's 442 # return value. 443 } 444 445 # Returns 0 (true) if ksadmin supports --version-path and --version-key. 446 ksadmin_supports_versionpath_versionkey() { 447 # --version-path and --version-key were introduced in Keystone 1.0.9.2318. 448 is_ksadmin_version_ge 1.0.9.2318 449 450 # The return value of is_ksadmin_version_ge is used as this function's 451 # return value. 452 } 453 454 # Runs "defaults read" to obtain the value of a key in a property list. As 455 # with "defaults read", an absolute path to a plist is supplied, without the 456 # ".plist" extension. 457 # 458 # As of Mac OS X 10.8, defaults (and NSUserDefaults and CFPreferences) 459 # normally communicates with cfprefsd to read and write plists. Changes to a 460 # plist file aren't necessarily reflected immediately via this API family when 461 # not made through this API family, because cfprefsd may return cached data 462 # from a former on-disk version of a plist file instead of reading the current 463 # version from disk. The old behavior can be restored by setting the 464 # __CFPREFERENCES_AVOID_DAEMON environment variable, although extreme care 465 # should be used because portions of the system that use this API family 466 # normally and thus use cfprefsd and its cache will become unsynchronized with 467 # the on-disk state. 468 # 469 # This function is provided to set __CFPREFERENCES_AVOID_DAEMON when calling 470 # "defaults read" and thus avoid cfprefsd and its on-disk cache, and is 471 # intended only to be used to read values from Info.plist files, which are not 472 # preferences. The use of "defaults" for this purpose has always been 473 # questionable, but there's no better option to interact with plists from 474 # shell scripts. Definitely don't use infoplist_read to read preference 475 # plists. 476 # 477 # This function exists because the update process delivers new copies of 478 # Info.plist files to the disk behind cfprefsd's back, and if cfprefsd becomes 479 # aware of the original version of the file for any reason (such as this 480 # script reading values from it via "defaults read"), the new version of the 481 # file will not be immediately effective or visible via cfprefsd after the 482 # update is applied. 483 infoplist_read() { 484 __CFPREFERENCES_AVOID_DAEMON=1 defaults read "${@}" 485 } 486 487 usage() { 488 echo "usage: ${ME} update_dmg_mount_point" >& 2 489 } 490 491 main() { 492 local update_dmg_mount_point="${1}" 493 494 # Early steps are critical. Don't continue past any failure. 495 set -e 496 497 trap cleanup EXIT HUP INT QUIT TERM 498 499 readonly PRODUCT_NAME="Google Chrome" 500 readonly APP_DIR="${PRODUCT_NAME}.app" 501 readonly ALTERNATE_APP_DIR="${PRODUCT_NAME} Canary.app" 502 readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework" 503 readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" 504 readonly PATCH_DIR=".patch" 505 readonly CONTENTS_DIR="Contents" 506 readonly APP_PLIST="${CONTENTS_DIR}/Info" 507 readonly VERSIONS_DIR="${CONTENTS_DIR}/Versions" 508 readonly UNROOTED_BRAND_PLIST="Library/Google/Google Chrome Brand" 509 readonly UNROOTED_DEBUG_FILE="Library/Google/Google Chrome Updater Debug" 510 511 readonly APP_VERSION_KEY="CFBundleShortVersionString" 512 readonly APP_BUNDLEID_KEY="CFBundleIdentifier" 513 readonly KS_VERSION_KEY="KSVersion" 514 readonly KS_PRODUCT_KEY="KSProductID" 515 readonly KS_URL_KEY="KSUpdateURL" 516 readonly KS_CHANNEL_KEY="KSChannelID" 517 readonly KS_BRAND_KEY="KSBrandID" 518 519 readonly QUARANTINE_ATTR="com.apple.quarantine" 520 readonly KEYCHAIN_REAUTHORIZE_DIR=".keychain_reauthorize" 521 522 # Don't use rsync -a, because -a expands to -rlptgoD. -g and -o copy owners 523 # and groups, respectively, from the source, and that is undesirable in this 524 # case. -D copies devices and special files; copying devices only works 525 # when running as root, so for consistency between privileged and 526 # unprivileged operation, this option is omitted as well. 527 # -I, --ignore-times don't skip files that match in size and mod-time 528 # -l, --links copy symlinks as symlinks 529 # -r, --recursive recurse into directories 530 # -p, --perms preserve permissions 531 # -t, --times preserve times 532 readonly RSYNC_FLAGS="-Ilprt" 533 534 # It's difficult to get GOOGLE_CHROME_UPDATER_DEBUG set in the environment 535 # when this script is called from Keystone. If a "debug file" exists in 536 # either the root directory or the home directory of the user who owns the 537 # ticket, turn on verbosity. This may aid debugging. 538 if [[ -e "/${UNROOTED_DEBUG_FILE}" ]] || 539 [[ -e ~/"${UNROOTED_DEBUG_FILE}" ]]; then 540 export GOOGLE_CHROME_UPDATER_DEBUG="y" 541 fi 542 543 note "update_dmg_mount_point = ${update_dmg_mount_point}" 544 545 # The argument should be the disk image path. Make sure it exists and that 546 # it's an absolute path. 547 note "checking update" 548 549 if [[ -z "${update_dmg_mount_point}" ]] || 550 [[ "${update_dmg_mount_point:0:1}" != "/" ]] || 551 ! [[ -d "${update_dmg_mount_point}" ]]; then 552 err "update_dmg_mount_point must be an absolute path to a directory" 553 usage 554 exit 2 555 fi 556 557 local patch_dir="${update_dmg_mount_point}/${PATCH_DIR}" 558 if [[ "${patch_dir:0:1}" != "/" ]]; then 559 note "patch_dir = ${patch_dir}" 560 err "patch_dir must be an absolute path" 561 exit 2 562 fi 563 564 # Figure out if this is an ordinary installation disk image being used as a 565 # full update, or a patch. A patch will have a .patch directory at the root 566 # of the disk image containing information about the update, tools to apply 567 # it, and the update contents. 568 local is_patch= 569 local dirpatcher= 570 if [[ -d "${patch_dir}" ]]; then 571 # patch_dir exists and is a directory - this is a patch update. 572 is_patch="y" 573 dirpatcher="${patch_dir}/dirpatcher.sh" 574 if ! [[ -x "${dirpatcher}" ]]; then 575 err "couldn't locate dirpatcher" 576 exit 6 577 fi 578 elif [[ -e "${patch_dir}" ]]; then 579 # patch_dir exists, but is not a directory - what's that mean? 580 note "patch_dir = ${patch_dir}" 581 err "patch_dir must be a directory" 582 exit 2 583 else 584 # patch_dir does not exist - this is a full "installer." 585 patch_dir= 586 fi 587 note "patch_dir = ${patch_dir}" 588 note "is_patch = ${is_patch}" 589 note "dirpatcher = ${dirpatcher}" 590 591 # The update to install. 592 593 # update_app is the path to the new version of the .app. It will only be 594 # set at this point for a non-patch update. It is not yet set for a patch 595 # update because no such directory exists yet; it will be set later when 596 # dirpatcher creates it. 597 local update_app= 598 599 # update_version_app_old, patch_app_dir, and patch_versioned_dir will only 600 # be set for patch updates. 601 local update_version_app_old= 602 local patch_app_dir= 603 local patch_versioned_dir= 604 605 local update_version_app update_version_ks product_id 606 if [[ -z "${is_patch}" ]]; then 607 update_app="${update_dmg_mount_point}/${APP_DIR}" 608 note "update_app = ${update_app}" 609 610 # Make sure that it's an absolute path. 611 if [[ "${update_app:0:1}" != "/" ]]; then 612 err "update_app must be an absolute path" 613 exit 2 614 fi 615 616 # Make sure there's something to copy from. 617 if ! [[ -d "${update_app}" ]]; then 618 update_app="${update_dmg_mount_point}/${ALTERNATE_APP_DIR}" 619 note "update_app = ${update_app}" 620 621 if [[ "${update_app:0:1}" != "/" ]]; then 622 err "update_app (alternate) must be an absolute path" 623 exit 2 624 fi 625 626 if ! [[ -d "${update_app}" ]]; then 627 err "update_app must be a directory" 628 exit 2 629 fi 630 fi 631 632 # Get some information about the update. 633 note "reading update values" 634 635 local update_app_plist="${update_app}/${APP_PLIST}" 636 note "update_app_plist = ${update_app_plist}" 637 if ! update_version_app="$(infoplist_read "${update_app_plist}" \ 638 "${APP_VERSION_KEY}")" || 639 [[ -z "${update_version_app}" ]]; then 640 err "couldn't determine update_version_app" 641 exit 2 642 fi 643 note "update_version_app = ${update_version_app}" 644 645 local update_ks_plist="${update_app_plist}" 646 note "update_ks_plist = ${update_ks_plist}" 647 if ! update_version_ks="$(infoplist_read "${update_ks_plist}" \ 648 "${KS_VERSION_KEY}")" || 649 [[ -z "${update_version_ks}" ]]; then 650 err "couldn't determine update_version_ks" 651 exit 2 652 fi 653 note "update_version_ks = ${update_version_ks}" 654 655 if ! product_id="$(infoplist_read "${update_ks_plist}" \ 656 "${KS_PRODUCT_KEY}")" || 657 [[ -z "${product_id}" ]]; then 658 err "couldn't determine product_id" 659 exit 2 660 fi 661 note "product_id = ${product_id}" 662 else # [[ -n "${is_patch}" ]] 663 # Get some information about the update. 664 note "reading update values" 665 666 if ! update_version_app_old=$(<"${patch_dir}/old_app_version") || 667 [[ -z "${update_version_app_old}" ]]; then 668 err "couldn't determine update_version_app_old" 669 exit 2 670 fi 671 note "update_version_app_old = ${update_version_app_old}" 672 673 if ! update_version_app=$(<"${patch_dir}/new_app_version") || 674 [[ -z "${update_version_app}" ]]; then 675 err "couldn't determine update_version_app" 676 exit 2 677 fi 678 note "update_version_app = ${update_version_app}" 679 680 if ! update_version_ks=$(<"${patch_dir}/new_ks_version") || 681 [[ -z "${update_version_ks}" ]]; then 682 err "couldn't determine update_version_ks" 683 exit 2 684 fi 685 note "update_version_ks = ${update_version_ks}" 686 687 if ! product_id=$(<"${patch_dir}/ks_product") || 688 [[ -z "${product_id}" ]]; then 689 err "couldn't determine product_id" 690 exit 2 691 fi 692 note "product_id = ${product_id}" 693 694 patch_app_dir="${patch_dir}/application.dirpatch" 695 if ! [[ -d "${patch_app_dir}" ]]; then 696 err "couldn't locate patch_app_dir" 697 exit 6 698 fi 699 note "patch_app_dir = ${patch_app_dir}" 700 701 patch_versioned_dir=\ 702 "${patch_dir}/version_${update_version_app_old}_${update_version_app}.dirpatch" 703 if ! [[ -d "${patch_versioned_dir}" ]]; then 704 err "couldn't locate patch_versioned_dir" 705 exit 6 706 fi 707 note "patch_versioned_dir = ${patch_versioned_dir}" 708 fi 709 710 # ksadmin is required. Keystone should have set a ${PATH} that includes it. 711 # Check that here, so that more useful feedback can be offered in the 712 # unlikely event that ksadmin is missing. 713 note "checking Keystone" 714 715 local ksadmin_path 716 if ! ksadmin_path="$(type -p ksadmin)" || [[ -z "${ksadmin_path}" ]]; then 717 err "couldn't locate ksadmin_path" 718 exit 3 719 fi 720 note "ksadmin_path = ${ksadmin_path}" 721 722 # Call ksadmin_version once to prime the global state. This is needed 723 # because subsequent calls to ksadmin_version that occur in $(...) 724 # expansions will not affect the global state (although they can read from 725 # the already-initialized global state) and thus will cause a new ksadmin 726 # --ksadmin-version process to run for each check unless the globals have 727 # been properly initialized beforehand. 728 ksadmin_version >& /dev/null || true 729 local ksadmin_version_string 730 ksadmin_version_string="$(ksadmin_version 2> /dev/null || true)" 731 note "ksadmin_version_string = ${ksadmin_version_string}" 732 733 # Figure out where to install. 734 local installed_app 735 if ! installed_app="$(ksadmin -pP "${product_id}" | sed -Ene \ 736 "s%^[[:space:]]+xc=<KSPathExistenceChecker:.* path=(/.+)>\$%\\1%p")" || 737 [[ -z "${installed_app}" ]]; then 738 err "couldn't locate installed_app" 739 exit 3 740 fi 741 note "installed_app = ${installed_app}" 742 743 if [[ "${installed_app:0:1}" != "/" ]] || 744 ! [[ -d "${installed_app}" ]]; then 745 err "installed_app must be an absolute path to a directory" 746 exit 3 747 fi 748 749 # If this script is running as root, it's being driven by a system ticket. 750 # Otherwise, it's being driven by a user ticket. 751 local system_ticket= 752 if [[ ${EUID} -eq 0 ]]; then 753 system_ticket="y" 754 fi 755 note "system_ticket = ${system_ticket}" 756 757 # If this script is being driven by a user ticket, but a system ticket is 758 # also present, there's a potential for the two to collide. Both ticket 759 # types might be present if another user on the system promoted the ticket 760 # to system: the other user could not have removed this user's user ticket. 761 # Handle that case here by deleting the user ticket and exiting early with 762 # a discrete exit code. 763 # 764 # Current versions of ksadmin will exit 1 (false) when asked to print tickets 765 # and given a specific product ID to print. Older versions of ksadmin would 766 # exit 0 (true), but those same versions did not support -S (meaning to check 767 # the system ticket store) and would exit 1 (false) with this invocation due 768 # to not understanding the question. Therefore, the usage here will only 769 # delete the existing user ticket when running as non-root with access to a 770 # sufficiently recent ksadmin. Older ksadmins are tolerated: the update will 771 # likely fail for another reason and the user ticket will hang around until 772 # something is eventually able to remove it. 773 if [[ -z "${system_ticket}" ]] && 774 ksadmin -S --print-tickets --productid "${product_id}" >& /dev/null; then 775 ksadmin --delete --productid "${product_id}" || true 776 err "can't update on a user ticket when a system ticket is also present" 777 exit 4 778 fi 779 780 # Figure out what the existing installed application is using for its 781 # versioned directory. This will be used later, to avoid removing the 782 # existing installed version's versioned directory in case anything is still 783 # using it. 784 note "reading install values" 785 786 local installed_app_plist="${installed_app}/${APP_PLIST}" 787 note "installed_app_plist = ${installed_app_plist}" 788 local installed_app_plist_path="${installed_app_plist}.plist" 789 note "installed_app_plist_path = ${installed_app_plist_path}" 790 local old_version_app 791 old_version_app="$(infoplist_read "${installed_app_plist}" \ 792 "${APP_VERSION_KEY}" || true)" 793 note "old_version_app = ${old_version_app}" 794 795 # old_version_app is not required, because it won't be present in skeleton 796 # bootstrap installations, which just have an empty .app directory. Only 797 # require it when doing a patch update, and use it to validate that the 798 # patch applies to the old installed version. By definition, skeleton 799 # bootstraps can't be installed with patch updates. They require the full 800 # application on the disk image. 801 if [[ -n "${is_patch}" ]]; then 802 if [[ -z "${old_version_app}" ]]; then 803 err "old_version_app required for patch" 804 exit 6 805 elif [[ "${old_version_app}" != "${update_version_app_old}" ]]; then 806 err "this patch does not apply to the installed version" 807 exit 6 808 fi 809 fi 810 811 local installed_versions_dir="${installed_app}/${VERSIONS_DIR}" 812 note "installed_versions_dir = ${installed_versions_dir}" 813 814 # If the installed application is incredibly old, old_versioned_dir may not 815 # exist. 816 local old_versioned_dir 817 if [[ -n "${old_version_app}" ]]; then 818 old_versioned_dir="${installed_versions_dir}/${old_version_app}" 819 fi 820 note "old_versioned_dir = ${old_versioned_dir}" 821 822 # Collect the installed application's brand code, it will be used later. It 823 # is not an error for the installed application to not have a brand code. 824 local old_ks_plist="${installed_app_plist}" 825 note "old_ks_plist = ${old_ks_plist}" 826 local old_brand 827 old_brand="$(infoplist_read "${old_ks_plist}" \ 828 "${KS_BRAND_KEY}" 2> /dev/null || 829 true)" 830 note "old_brand = ${old_brand}" 831 832 ensure_writable_symlinks_recursive "${installed_app}" 833 834 # By copying to ${installed_app}, the existing application name will be 835 # preserved, if the user has renamed the application on disk. Respecting 836 # the user's changes is friendly. 837 838 # Make sure that ${installed_versions_dir} exists, so that it can receive 839 # the versioned directory. It may not exist if updating from an older 840 # version that did not use the versioned layout on disk. Later, during the 841 # rsync to copy the application directory, the mode bits and timestamp on 842 # ${installed_versions_dir} will be set to conform to whatever is present in 843 # the update. 844 # 845 # ${installed_app} is guaranteed to exist at this point, but 846 # ${installed_app}/${CONTENTS_DIR} may not if things are severely broken or 847 # if this update is actually an initial installation from a Keystone 848 # skeleton bootstrap. The mkdir creates ${installed_app}/${CONTENTS_DIR} if 849 # it doesn't exist; its mode bits will be fixed up in a subsequent rsync. 850 note "creating installed_versions_dir" 851 if ! mkdir -p "${installed_versions_dir}"; then 852 err "mkdir of installed_versions_dir failed" 853 exit 5 854 fi 855 856 local new_versioned_dir 857 new_versioned_dir="${installed_versions_dir}/${update_version_app}" 858 note "new_versioned_dir = ${new_versioned_dir}" 859 860 # If there's an entry at ${new_versioned_dir} but it's not a directory 861 # (or it's a symbolic link, whether or not it points to a directory), rsync 862 # won't get rid of it. It's never correct to have a non-directory in place 863 # of the versioned directory, so toss out whatever's there. Don't treat 864 # this as a critical step: if removal fails, operation can still proceed to 865 # to the dirpatcher or rsync, which will likely fail. 866 if [[ -e "${new_versioned_dir}" ]] && 867 ([[ -L "${new_versioned_dir}" ]] || 868 ! [[ -d "${new_versioned_dir}" ]]); then 869 note "removing non-directory in place of versioned directory" 870 rm -f "${new_versioned_dir}" 2> /dev/null || true 871 fi 872 873 local update_versioned_dir 874 if [[ -z "${is_patch}" ]]; then 875 update_versioned_dir="${update_app}/${VERSIONS_DIR}/${update_version_app}" 876 note "update_versioned_dir = ${update_versioned_dir}" 877 else # [[ -n "${is_patch}" ]] 878 # dirpatcher won't patch into a directory that already exists. Doing so 879 # would be a bad idea, anyway. If ${new_versioned_dir} already exists, 880 # it may be something left over from a previous failed or incomplete 881 # update attempt, or it may be the live versioned directory if this is a 882 # same-version update intended only to change channels. Since there's no 883 # way to tell, this case is handled by having dirpatcher produce the new 884 # versioned directory in a temporary location and then having rsync copy 885 # it into place as an ${update_versioned_dir}, the same as in a non-patch 886 # update. If ${new_versioned_dir} doesn't exist, dirpatcher can place the 887 # new versioned directory at that location directly. 888 local versioned_dir_target 889 if ! [[ -e "${new_versioned_dir}" ]]; then 890 versioned_dir_target="${new_versioned_dir}" 891 note "versioned_dir_target = ${versioned_dir_target}" 892 else 893 ensure_temp_dir 894 versioned_dir_target="${g_temp_dir}/${update_version_app}" 895 note "versioned_dir_target = ${versioned_dir_target}" 896 update_versioned_dir="${versioned_dir_target}" 897 note "update_versioned_dir = ${update_versioned_dir}" 898 fi 899 900 note "dirpatching versioned directory" 901 if ! "${dirpatcher}" "${old_versioned_dir}" \ 902 "${patch_versioned_dir}" \ 903 "${versioned_dir_target}"; then 904 err "dirpatcher of versioned directory failed, status ${PIPESTATUS[0]}" 905 exit 12 906 fi 907 fi 908 909 # Copy the versioned directory. The new versioned directory should have a 910 # different name than any existing one, so this won't harm anything already 911 # present in ${installed_versions_dir}, including the versioned directory 912 # being used by any running processes. If this step is interrupted, there 913 # will be an incomplete versioned directory left behind, but it won't 914 # won't interfere with anything, and it will be replaced or removed during a 915 # future update attempt. 916 # 917 # In certain cases, same-version updates are distributed to move users 918 # between channels; when this happens, the contents of the versioned 919 # directories are identical and rsync will not render the versioned 920 # directory unusable even for an instant. 921 # 922 # ${update_versioned_dir} may be empty during a patch update (${is_patch}) 923 # if the dirpatcher above was able to write it into place directly. In 924 # that event, dirpatcher guarantees that ${new_versioned_dir} is already in 925 # place. 926 if [[ -n "${update_versioned_dir}" ]]; then 927 note "rsyncing versioned directory" 928 if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ 929 "${new_versioned_dir}"; then 930 err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" 931 exit 7 932 fi 933 fi 934 935 if [[ -n "${is_patch}" ]]; then 936 # If the versioned directory was prepared in a temporary directory and 937 # then rsynced into place, remove the temporary copy now that it's no 938 # longer needed. 939 if [[ -n "${update_versioned_dir}" ]]; then 940 rm -rf "${update_versioned_dir}" 2> /dev/null || true 941 update_versioned_dir= 942 note "update_versioned_dir = ${update_versioned_dir}" 943 fi 944 945 # Prepare ${update_app}. This always needs to be done in a temporary 946 # location because dirpatcher won't write to a directory that already 947 # exists, and ${installed_app} needs to be used as input to dirpatcher 948 # in any event. The new application will be rsynced into place once 949 # dirpatcher creates it. 950 ensure_temp_dir 951 update_app="${g_temp_dir}/${APP_DIR}" 952 note "update_app = ${update_app}" 953 954 note "dirpatching app directory" 955 if ! "${dirpatcher}" "${installed_app}" \ 956 "${patch_app_dir}" \ 957 "${update_app}"; then 958 err "dirpatcher of app directory failed, status ${PIPESTATUS[0]}" 959 exit 13 960 fi 961 fi 962 963 # See if the timestamp of what's currently on disk is newer than the 964 # update's outer .app's timestamp. rsync will copy the update's timestamp 965 # over, but if that timestamp isn't as recent as what's already on disk, the 966 # .app will need to be touched. 967 local needs_touch= 968 if [[ "${installed_app}" -nt "${update_app}" ]]; then 969 needs_touch="y" 970 fi 971 note "needs_touch = ${needs_touch}" 972 973 # Copy the unversioned files into place, leaving everything in 974 # ${installed_versions_dir} alone. If this step is interrupted, the 975 # application will at least remain in a usable state, although it may not 976 # pass signature validation. Depending on when this step is interrupted, 977 # the application will either launch the old or the new version. The 978 # critical point is when the main executable is replaced. There isn't very 979 # much to copy in this step, because most of the application is in the 980 # versioned directory. This step only accounts for around 50 files, most of 981 # which are small localized InfoPlist.strings files. Note that 982 # ${VERSIONS_DIR} is included to copy its mode bits and timestamp, but its 983 # contents are excluded, having already been installed above. 984 note "rsyncing app directory" 985 if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}/*" \ 986 "${update_app}/" "${installed_app}"; then 987 err "rsync of app directory failed, status ${PIPESTATUS[0]}" 988 exit 8 989 fi 990 991 note "rsyncs complete" 992 993 if [[ -n "${is_patch}" ]]; then 994 # update_app has been rsynced into place and is no longer needed. 995 rm -rf "${update_app}" 2> /dev/null || true 996 update_app= 997 note "update_app = ${update_app}" 998 fi 999 1000 if [[ -n "${g_temp_dir}" ]]; then 1001 # The temporary directory, if any, is no longer needed. 1002 rm -rf "${g_temp_dir}" 2> /dev/null || true 1003 g_temp_dir= 1004 note "g_temp_dir = ${g_temp_dir}" 1005 fi 1006 1007 # If necessary, touch the outermost .app so that it appears to the outside 1008 # world that something was done to the bundle. This will cause 1009 # LaunchServices to invalidate the information it has cached about the 1010 # bundle even if lsregister does not run. This is not done if rsync already 1011 # updated the timestamp to something newer than what had been on disk. This 1012 # is not considered a critical step, and if it fails, this script will not 1013 # exit. 1014 if [[ -n "${needs_touch}" ]]; then 1015 touch -cf "${installed_app}" || true 1016 fi 1017 1018 # Read the new values, such as the version. 1019 note "reading new values" 1020 1021 local new_version_app 1022 if ! new_version_app="$(infoplist_read "${installed_app_plist}" \ 1023 "${APP_VERSION_KEY}")" || 1024 [[ -z "${new_version_app}" ]]; then 1025 err "couldn't determine new_version_app" 1026 exit 9 1027 fi 1028 note "new_version_app = ${new_version_app}" 1029 1030 local new_versioned_dir="${installed_versions_dir}/${new_version_app}" 1031 note "new_versioned_dir = ${new_versioned_dir}" 1032 1033 local new_ks_plist="${installed_app_plist}" 1034 note "new_ks_plist = ${new_ks_plist}" 1035 1036 local new_version_ks 1037 if ! new_version_ks="$(infoplist_read "${new_ks_plist}" \ 1038 "${KS_VERSION_KEY}")" || 1039 [[ -z "${new_version_ks}" ]]; then 1040 err "couldn't determine new_version_ks" 1041 exit 9 1042 fi 1043 note "new_version_ks = ${new_version_ks}" 1044 1045 local update_url 1046 if ! update_url="$(infoplist_read "${new_ks_plist}" "${KS_URL_KEY}")" || 1047 [[ -z "${update_url}" ]]; then 1048 err "couldn't determine update_url" 1049 exit 9 1050 fi 1051 note "update_url = ${update_url}" 1052 1053 # The channel ID is optional. Suppress stderr to prevent Keystone from 1054 # seeing possible error output. 1055 local channel 1056 channel="$(infoplist_read "${new_ks_plist}" \ 1057 "${KS_CHANNEL_KEY}" 2> /dev/null || true)" 1058 note "channel = ${channel}" 1059 1060 # Make sure that the update was successful by comparing the version found in 1061 # the update with the version now on disk. 1062 if [[ "${new_version_ks}" != "${update_version_ks}" ]]; then 1063 err "new_version_ks and update_version_ks do not match" 1064 exit 10 1065 fi 1066 1067 # Notify LaunchServices. This is not considered a critical step, and 1068 # lsregister's exit codes shouldn't be confused with this script's own. 1069 # Redirect stdout to /dev/null to suppress the useless "ThrottleProcessIO: 1070 # throttling disk i/o" messages that lsregister might print. 1071 note "notifying LaunchServices" 1072 local coreservices="/System/Library/Frameworks/CoreServices.framework" 1073 local launchservices="${coreservices}/Frameworks/LaunchServices.framework" 1074 local lsregister="${launchservices}/Support/lsregister" 1075 note "coreservices = ${coreservices}" 1076 note "launchservices = ${launchservices}" 1077 note "lsregister = ${lsregister}" 1078 "${lsregister}" -f "${installed_app}" > /dev/null || true 1079 1080 # The brand information is stored differently depending on whether this is 1081 # running for a system or user ticket. 1082 note "handling brand code" 1083 1084 local set_brand_file_access= 1085 local brand_plist 1086 if [[ -n "${system_ticket}" ]]; then 1087 # System ticket. 1088 set_brand_file_access="y" 1089 brand_plist="/${UNROOTED_BRAND_PLIST}" 1090 else 1091 # User ticket. 1092 brand_plist=~/"${UNROOTED_BRAND_PLIST}" 1093 fi 1094 local brand_plist_path="${brand_plist}.plist" 1095 note "set_brand_file_access = ${set_brand_file_access}" 1096 note "brand_plist = ${brand_plist}" 1097 note "brand_plist_path = ${brand_plist_path}" 1098 1099 local ksadmin_brand_plist_path 1100 local ksadmin_brand_key 1101 1102 # Only the stable channel, identified by an empty channel string, has a 1103 # brand code. On the beta and dev channels, remove the brand plist if 1104 # present. Its presence means that the ticket used to manage a 1105 # stable-channel Chrome but the user has since replaced it with a beta or 1106 # dev channel version. Since the canary channel can run side-by-side with 1107 # another Chrome installation, don't remove the brand plist on that channel, 1108 # but skip the rest of the brand logic. 1109 if [[ "${channel}" = "beta" ]] || [[ "${channel}" = "dev" ]]; then 1110 note "defeating brand code on channel ${channel}" 1111 rm -f "${brand_plist_path}" 2>/dev/null || true 1112 elif [[ -n "${channel}" ]]; then 1113 # Canary channel. 1114 note "skipping brand code on channel ${channel}" 1115 else 1116 # Stable channel. 1117 # If the user manually updated their copy of Chrome, there might be new 1118 # brand information in the app bundle, and that needs to be copied out 1119 # into the file Keystone looks at. 1120 if [[ -n "${old_brand}" ]]; then 1121 local brand_dir 1122 brand_dir="$(dirname "${brand_plist_path}")" 1123 note "brand_dir = ${brand_dir}" 1124 if ! mkdir -p "${brand_dir}"; then 1125 err "couldn't mkdir brand_dir, continuing" 1126 else 1127 if ! defaults write "${brand_plist}" "${KS_BRAND_KEY}" \ 1128 -string "${old_brand}"; then 1129 err "couldn't write brand_plist, continuing" 1130 elif [[ -n "${set_brand_file_access}" ]]; then 1131 if ! chown "root:wheel" "${brand_plist_path}"; then 1132 err "couldn't chown brand_plist_path, continuing" 1133 else 1134 if ! chmod 644 "${brand_plist_path}"; then 1135 err "couldn't chmod brand_plist_path, continuing" 1136 fi 1137 fi 1138 fi 1139 fi 1140 fi 1141 1142 # Confirm that the brand file exists. It's optional. 1143 ksadmin_brand_plist_path="${brand_plist_path}" 1144 ksadmin_brand_key="${KS_BRAND_KEY}" 1145 1146 if [[ ! -f "${ksadmin_brand_plist_path}" ]]; then 1147 # Clear any branding information. 1148 ksadmin_brand_plist_path= 1149 ksadmin_brand_key= 1150 fi 1151 fi 1152 1153 note "ksadmin_brand_plist_path = ${ksadmin_brand_plist_path}" 1154 note "ksadmin_brand_key = ${ksadmin_brand_key}" 1155 1156 note "notifying Keystone" 1157 1158 local ksadmin_args=( 1159 --register 1160 --productid "${product_id}" 1161 --version "${new_version_ks}" 1162 --xcpath "${installed_app}" 1163 --url "${update_url}" 1164 ) 1165 1166 if ksadmin_supports_tag; then 1167 ksadmin_args+=( 1168 --tag "${channel}" 1169 ) 1170 fi 1171 1172 if ksadmin_supports_tagpath_tagkey; then 1173 ksadmin_args+=( 1174 --tag-path "${installed_app_plist_path}" 1175 --tag-key "${KS_CHANNEL_KEY}" 1176 ) 1177 fi 1178 1179 if ksadmin_supports_brandpath_brandkey; then 1180 ksadmin_args+=( 1181 --brand-path "${ksadmin_brand_plist_path}" 1182 --brand-key "${ksadmin_brand_key}" 1183 ) 1184 fi 1185 1186 if ksadmin_supports_versionpath_versionkey; then 1187 ksadmin_args+=( 1188 --version-path "${installed_app_plist_path}" 1189 --version-key "${KS_VERSION_KEY}" 1190 ) 1191 fi 1192 1193 note "ksadmin_args = ${ksadmin_args[*]}" 1194 1195 if ! ksadmin "${ksadmin_args[@]}"; then 1196 err "ksadmin failed" 1197 exit 11 1198 fi 1199 1200 # The remaining steps are not considered critical. 1201 set +e 1202 1203 # Try to clean up old versions that are not in use. The strategy is to keep 1204 # the versioned directory corresponding to the update just applied 1205 # (obviously) and the version that was just replaced, and to use ps and lsof 1206 # to see if it looks like any processes are currently using any other old 1207 # directories. Directories not in use are removed. Old versioned 1208 # directories that are in use are left alone so as to not interfere with 1209 # running processes. These directories can be cleaned up by this script on 1210 # future updates. 1211 # 1212 # To determine which directories are in use, both ps and lsof are used. 1213 # Each approach has limitations. 1214 # 1215 # The ps check looks for processes within the versioned directory. Only 1216 # helper processes, such as renderers, are within the versioned directory. 1217 # Browser processes are not, so the ps check will not find them, and will 1218 # assume that a versioned directory is not in use if a browser is open 1219 # without any windows. The ps mechanism can also only detect processes 1220 # running on the system that is performing the update. If network shares 1221 # are involved, all bets are off. 1222 # 1223 # The lsof check looks to see what processes have the framework dylib open. 1224 # Browser processes will have their versioned framework dylib open, so this 1225 # check is able to catch browsers even if there are no associated helper 1226 # processes. Like the ps check, the lsof check is limited to processes on 1227 # the system that is performing the update. Finally, unless running as 1228 # root, the lsof check can only find processes running as the effective user 1229 # performing the update. 1230 # 1231 # These limitations are motivations to additionally preserve the versioned 1232 # directory corresponding to the version that was just replaced. 1233 note "cleaning up old versioned directories" 1234 1235 local versioned_dir 1236 for versioned_dir in "${installed_versions_dir}/"*; do 1237 note "versioned_dir = ${versioned_dir}" 1238 if [[ "${versioned_dir}" = "${new_versioned_dir}" ]] || \ 1239 [[ "${versioned_dir}" = "${old_versioned_dir}" ]]; then 1240 # This is the versioned directory corresponding to the update that was 1241 # just applied or the version that was previously in use. Leave it 1242 # alone. 1243 note "versioned_dir is new_versioned_dir or old_versioned_dir, skipping" 1244 continue 1245 fi 1246 1247 # Look for any processes whose executables are within this versioned 1248 # directory. They'll be helper processes, such as renderers. Their 1249 # existence indicates that this versioned directory is currently in use. 1250 local ps_string="${versioned_dir}/" 1251 note "ps_string = ${ps_string}" 1252 1253 # Look for any processes using the framework dylib. This will catch 1254 # browser processes where the ps check will not, but it is limited to 1255 # processes running as the effective user. 1256 local lsof_file="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}" 1257 note "lsof_file = ${lsof_file}" 1258 1259 # ps -e displays all users' processes, -ww causes ps to not truncate 1260 # lines, -o comm instructs it to only print the command name, and the = 1261 # tells it to not print a header line. 1262 # The cut invocation filters the ps output to only have at most the number 1263 # of characters in ${ps_string}. This is done so that grep can look for 1264 # an exact match. 1265 # grep -F tells grep to look for lines that are exact matches (not regular 1266 # expressions), -q tells it to not print any output and just indicate 1267 # matches by exit status, and -x tells it that the entire line must match 1268 # ${ps_string} exactly, as opposed to matching a substring. A match 1269 # causes grep to exit zero (true). 1270 # 1271 # lsof will exit nonzero if ${lsof_file} does not exist or is open by any 1272 # process. If the file exists and is open, it will exit zero (true). 1273 if (! ps -ewwo comm= | \ 1274 cut -c "1-${#ps_string}" | \ 1275 grep -Fqx "${ps_string}") && 1276 (! lsof "${lsof_file}" >& /dev/null); then 1277 # It doesn't look like anything is using this versioned directory. Get 1278 # rid of it. 1279 note "versioned_dir doesn't appear to be in use, removing" 1280 rm -rf "${versioned_dir}" 1281 else 1282 note "versioned_dir is in use, skipping" 1283 fi 1284 done 1285 1286 # If this script is being driven by a user Keystone ticket, it is not 1287 # running as root. If the application is installed somewhere under 1288 # /Applications, try to make it writable by all admin users. This will 1289 # allow other admin users to update the application from their own user 1290 # Keystone instances. 1291 # 1292 # If the script is being driven by a user Keystone ticket (not running as 1293 # root) and the application is not installed under /Applications, it might 1294 # not be in a system-wide location, and it probably won't be something that 1295 # other users on the system are running, so err on the side of safety and 1296 # don't make it group-writable. 1297 # 1298 # If this script is being driven by a system ticket (running as root), it's 1299 # future updates can be expected to be applied the same way, so admin- 1300 # writability is not a concern. Set the entire thing to be owned by root 1301 # in that case, regardless of where it's installed, and drop any group and 1302 # other write permission. 1303 # 1304 # If this script is running as a user that is not a member of the admin 1305 # group, the chgrp operation will not succeed. Tolerate that case, because 1306 # it's better than the alternative, which is to make the application 1307 # world-writable. 1308 note "setting permissions" 1309 1310 local chmod_mode="a+rX,u+w,go-w" 1311 if [[ -z "${system_ticket}" ]]; then 1312 if [[ "${installed_app:0:14}" = "/Applications/" ]] && 1313 chgrp -Rh admin "${installed_app}" 2> /dev/null; then 1314 chmod_mode="a+rX,ug+w,o-w" 1315 fi 1316 else 1317 chown -Rh root:wheel "${installed_app}" 2> /dev/null 1318 fi 1319 1320 note "chmod_mode = ${chmod_mode}" 1321 chmod -R "${chmod_mode}" "${installed_app}" 2> /dev/null 1322 1323 # On the Mac, or at least on HFS+, symbolic link permissions are significant, 1324 # but chmod -R and -h can't be used together. Do another pass to fix the 1325 # permissions on any symbolic links. 1326 find "${installed_app}" -type l -exec chmod -h "${chmod_mode}" {} + \ 1327 2> /dev/null 1328 1329 # If an update is triggered from within the application itself, the update 1330 # process inherits the quarantine bit (LSFileQuarantineEnabled). Any files 1331 # or directories created during the update will be quarantined in that case, 1332 # which may cause Launch Services to display quarantine UI. That's bad, 1333 # especially if it happens when the outer .app launches a quarantined inner 1334 # helper. If the application is already on the system and is being updated, 1335 # then it can be assumed that it should not be quarantined. Use xattr to 1336 # drop the quarantine attribute. 1337 # 1338 # TODO(mark): Instead of letting the quarantine attribute be set and then 1339 # dropping it here, figure out a way to get the update process to run 1340 # without LSFileQuarantineEnabled even when triggering an update from within 1341 # the application. 1342 note "lifting quarantine" 1343 1344 if os_xattr_supports_r; then 1345 # On 10.6, xattr supports -r for recursive operation. 1346 xattr -d -r "${QUARANTINE_ATTR}" "${installed_app}" 2> /dev/null 1347 else 1348 # On earlier systems, xattr doesn't support -r, so run xattr via find. 1349 find "${installed_app}" -exec xattr -d "${QUARANTINE_ATTR}" {} + \ 1350 2> /dev/null 1351 fi 1352 1353 # Do Keychain reauthorization. This involves running a stub executable on 1354 # the dmg that loads the newly-updated framework and jumps to it to perform 1355 # the reauthorization. The stub executable can be signed by the old 1356 # certificate even after the rest of Chrome switches to the new certificate, 1357 # so it still has access to the old Keychain items. The stub executable is 1358 # an unbundled flat file executable whose name matches the real 1359 # application's bundle identifier, so it's permitted access to the Keychain 1360 # items. Doing a reauthorization step at update time reauthorizes Keychain 1361 # items for users who never bother restarting Chrome, and provides a 1362 # mechanism to continue doing reauthorizations even after the certificate 1363 # changes. However, it only works for non-system ticket installations of 1364 # Chrome, because the updater runs as root when on a system ticket, and root 1365 # can't access individual user Keychains. 1366 # 1367 # Even if the reauthorization tool is launched, it doesn't necessarily try 1368 # to do anything. It will only attempt to perform a reauthorization if one 1369 # hasn't yet been done at update time. 1370 note "maybe reauthorizing Keychain" 1371 1372 if [[ -z "${system_ticket}" ]]; then 1373 local new_bundleid_app 1374 new_bundleid_app="$(infoplist_read "${installed_app_plist}" \ 1375 "${APP_BUNDLEID_KEY}" || true)" 1376 note "new_bundleid_app = ${new_bundleid_app}" 1377 1378 local keychain_reauthorize_dir="\ 1379 ${update_dmg_mount_point}/${KEYCHAIN_REAUTHORIZE_DIR}" 1380 local keychain_reauthorize_path="\ 1381 ${keychain_reauthorize_dir}/${new_bundleid_app}" 1382 note "keychain_reauthorize_path = ${keychain_reauthorize_path}" 1383 1384 if [[ -x "${keychain_reauthorize_path}" ]]; then 1385 local framework_dir="${new_versioned_dir}/${FRAMEWORK_DIR}" 1386 local framework_code_path="${framework_dir}/${FRAMEWORK_NAME}" 1387 note "framework_code_path = ${framework_code_path}" 1388 1389 if [[ -f "${framework_code_path}" ]]; then 1390 note "reauthorizing Keychain" 1391 "${keychain_reauthorize_path}" "${framework_code_path}" 1392 fi 1393 fi 1394 else 1395 note "system ticket, not reauthorizing Keychain" 1396 fi 1397 1398 # Great success! 1399 note "done!" 1400 1401 trap - EXIT 1402 1403 return 0 1404 } 1405 1406 # Check "less than" instead of "not equal to" in case Keystone ever changes to 1407 # pass more arguments. 1408 if [[ ${#} -lt 1 ]]; then 1409 usage 1410 exit 2 1411 fi 1412 1413 main "${@}" 1414 exit ${?} 1415