| OLD | NEW |
| 1 #!/bin/bash | 1 #!/bin/bash |
| 2 | 2 |
| 3 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | 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 | 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
| 6 | 6 |
| 7 # Called by the Keystone system to update the installed application with a new | 7 # Called by the Keystone system to update the installed application with a new |
| 8 # version from a disk image. | 8 # version from a disk image. |
| 9 | 9 |
| 10 # Return values: | 10 # Return values: |
| 11 # 0 Happiness | 11 # 0 Happiness |
| 12 # 1 Unknown failure | 12 # 1 Unknown failure |
| 13 # 2 Basic sanity check destination failure (e.g. ticket points to nothing) | 13 # 2 Basic sanity check destination failure (e.g. ticket points to nothing) |
| 14 # 3 Cannot get version of currently installed Chrome | 14 # 3 Cannot get version of currently installed Chrome |
| 15 # 4 No permission to write in destination directory | 15 # 4 rsync failed (could not assure presence of Versions directory) |
| 16 # 5 rsync failed | 16 # 5 rsync failed (could not copy new versioned directory to Versions) |
| 17 # 6 Cannot get version or update URL of newly installed Chrome | 17 # 6 rsync failed (could not update outer .app bundle) |
| 18 # 7 Post-install Chrome has same version as pre-install Chrome | 18 # 7 Cannot get version or update URL of newly installed Chrome |
| 19 # 8 ksadmin failure | 19 # 8 Post-install Chrome has same version as pre-install Chrome |
| 20 # 9 ksadmin failure |
| 20 # 10 Basic sanity check source failure (e.g. no app on disk image) | 21 # 10 Basic sanity check source failure (e.g. no app on disk image) |
| 21 | 22 |
| 22 set -e | 23 set -e |
| 23 | 24 |
| 24 # The argument should be the disk image path. Make sure it exists. | 25 # The argument should be the disk image path. Make sure it exists. |
| 25 if [ $# -lt 1 ] || [ ! -d "${1}" ]; then | 26 if [ $# -lt 1 ] || [ ! -d "${1}" ]; then |
| 26 exit 10 | 27 exit 10 |
| 27 fi | 28 fi |
| 28 | 29 |
| 29 # Who we are. | 30 # Who we are. |
| 30 PRODUCT_NAME="Google Chrome" | 31 PRODUCT_NAME="Google Chrome" |
| 31 APP_NAME="${PRODUCT_NAME}.app" | 32 APP_DIR="${PRODUCT_NAME}.app" |
| 32 FRAMEWORK_NAME="${PRODUCT_NAME} Framework.framework" | 33 FRAMEWORK_NAME="${PRODUCT_NAME} Framework" |
| 33 SRC="${1}/${APP_NAME}" | 34 FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" |
| 35 SRC="${1}/${APP_DIR}" |
| 34 | 36 |
| 35 # Sanity, make sure that there's something to copy from. | 37 # Make sure that there's something to copy from, and that it's an absolute |
| 36 if [ -z "${SRC}" ] || [ ! -d "${SRC}" ]; then | 38 # path. |
| 39 if [ -z "${SRC}" ] || [ "${SRC:0:1}" != "/" ] || [ ! -d "${SRC}" ] ; then |
| 37 exit 10 | 40 exit 10 |
| 38 fi | 41 fi |
| 39 | 42 |
| 40 # Figure out where we're going. Determine the application version to be | 43 # Figure out where we're going. Determine the application version to be |
| 41 # installed, use that to locate the framework, and then look inside the | 44 # installed, use that to locate the framework, and then look inside the |
| 42 # framework for the Keystone product ID. | 45 # framework for the Keystone product ID. |
| 43 APP_VERSION_KEY="CFBundleShortVersionString" | 46 APP_VERSION_KEY="CFBundleShortVersionString" |
| 44 UPD_VERSION_APP=$(defaults read "${SRC}/Contents/Info" "${APP_VERSION_KEY}" || | 47 UPD_VERSION_APP=$(defaults read "${SRC}/Contents/Info" "${APP_VERSION_KEY}" || |
| 45 exit 10) | 48 exit 10) |
| 46 UPD_KS_PLIST="${SRC}/Contents/Versions/${UPD_VERSION_APP}/${FRAMEWORK_NAME}/Reso
urces/Info" | 49 UPD_KS_PLIST="${SRC}/Contents/Versions/${UPD_VERSION_APP}/${FRAMEWORK_DIR}/Resou
rces/Info" |
| 47 PRODUCT_ID=$(defaults read "${UPD_KS_PLIST}" KSProductID || exit 10) | 50 PRODUCT_ID=$(defaults read "${UPD_KS_PLIST}" KSProductID || exit 10) |
| 48 DEST=$(ksadmin -pP "${PRODUCT_ID}" | grep xc= | sed -E 's/.+path=(.+)>$/\1/g') | 51 DEST=$(ksadmin -pP "${PRODUCT_ID}" | |
| 52 sed -Ene \ |
| 53 's%^[[:space:]]+xc=<KSPathExistenceChecker:.* path=(/.+)>$%\1%p') |
| 49 | 54 |
| 50 # More sanity checking. | 55 # More sanity checking. |
| 51 if [ -z "${DEST}" ] || [ ! -d "$(dirname "${DEST}")" ]; then | 56 if [ -z "${DEST}" ] || [ ! -d "${DEST}" ]; then |
| 52 exit 2 | 57 exit 2 |
| 53 fi | 58 fi |
| 54 | 59 |
| 55 # Read old version to help confirm install happiness. Older versions kept | 60 # Read old version to help confirm install happiness. Older versions kept |
| 56 # the KSVersion key in the application's Info.plist. Newer versions keep it | 61 # the KSVersion key in the application's Info.plist. Newer versions keep it |
| 57 # in the versioned framework's Info.plist. | 62 # in the versioned framework's Info.plist. |
| 63 OLD_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" || |
| 64 true) |
| 65 OLD_VERSIONED_DIR="${DEST}/Contents/Versions/${OLD_VERSION_APP}" |
| 66 OLD_KS_PLIST="${OLD_VERSIONED_DIR}/${FRAMEWORK_DIR}/Resources/Info" |
| 67 if [ -z "${OLD_VERSION_APP}" ] || [ ! -e "${OLD_KS_PLIST}.plist" ] ; then |
| 68 OLD_KS_PLIST="${DEST}/Contents/Info" |
| 69 fi |
| 58 KS_VERSION_KEY="KSVersion" | 70 KS_VERSION_KEY="KSVersion" |
| 59 OLD_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" || | |
| 60 defaults read "${DEST}/Contents/Info" "${KS_VERSION_KEY}" || | |
| 61 exit 3) | |
| 62 OLD_KS_PLIST="${DEST}/Contents/Versions/${OLD_VERSION_APP}/${FRAMEWORK_NAME}/Res
ources/Info" | |
| 63 if [ ! -e "${OLD_KS_PLIST}.plist" ] ; then | |
| 64 OLD_KS_PLIST="${DEST}/Contents/Info" | |
| 65 fi | |
| 66 OLD_VERSION_KS=$(defaults read "${OLD_KS_PLIST}" "${KS_VERSION_KEY}" || exit 3) | 71 OLD_VERSION_KS=$(defaults read "${OLD_KS_PLIST}" "${KS_VERSION_KEY}" || exit 3) |
| 67 | 72 |
| 68 # Make sure we have permission to write the destination | 73 # Don't use rsync -a, because -a expands to -rlptgoD. -g and -o copy owners |
| 69 DEST_DIRECTORY="$(dirname "${DEST}")" | 74 # and groups, respectively, from the source, and that is undesirable in this |
| 70 if [ ! -w "${DEST_DIRECTORY}" ]; then | 75 # case. -D copies devices and special files; copying devices only works |
| 71 exit 4 | 76 # when running as root, so for consistency between privileged and unprivileged |
| 72 fi | 77 # operation, this option is omitted as well. |
| 73 | 78 # -c, --checksum skip based on checksum, not mod-time & size |
| 74 # This usage will preserve any changes the user made to the application name. | 79 # -l, --links copy symlinks as symlinks |
| 75 # TODO(jrg): this may choke a running Chrome.app; be smarter. | 80 # -r, --recursive recurse into directories |
| 76 # Note: If the rsync fails we do not update the ticket version. | 81 # -p, --perms preserve permissions |
| 77 rsync -ac --delete "${SRC}/" "${DEST}/" || exit 5 | 82 # -t, --times preserve times |
| 83 RSYNC_FLAGS="-clprt" |
| 84 |
| 85 # By copying to ${DEST}, the existing application name will be preserved, even |
| 86 # if the user has renamed the application on disk. Respecting the user's |
| 87 # changes is friendly. |
| 88 |
| 89 # Make sure that the Versions directory exists, so that it can receive the |
| 90 # versioned directory. It may not exist if updating from an older version |
| 91 # that did not use the versioned layout on disk. An rsync that excludes all |
| 92 # contents is used to bring the permissions over from the update's Versions |
| 93 # directory, otherwise, this directory would be the only one in the entire |
| 94 # update exempt from getting its permissions copied over. A simple mkdir |
| 95 # wouldn't copy mode bits. This is done even if ${DEST}/Contents/Versions |
| 96 # already does exist to ensure that the mode bits come from the udpate. |
| 97 rsync ${RSYNC_FLAGS} --exclude "*" "${SRC}/Contents/Versions/" \ |
| 98 "${DEST}/Contents/Versions" || exit 4 |
| 99 |
| 100 # Copy the versioned directory. The new versioned directory will have a |
| 101 # different name than any existing one, so this won't harm anything already |
| 102 # present in Contents/Versions, including the versioned directory being used |
| 103 # by any running processes. If this step is interrupted, there will be an |
| 104 # incomplete versioned directory left behind, but it won't interfere with |
| 105 # anything, and it will be replaced or removed during a future update attempt. |
| 106 NEW_VERSIONED_DIR="${DEST}/Contents/Versions/${UPD_VERSION_APP}" |
| 107 rsync ${RSYNC_FLAGS} --delete-before \ |
| 108 "${SRC}/Contents/Versions/${UPD_VERSION_APP}/" \ |
| 109 "${NEW_VERSIONED_DIR}" || exit 5 |
| 110 |
| 111 # See if the timestamp of what's currently on disk is newer than the update's |
| 112 # outer .app's timestamp. rsync will copy the update's timestamp over, but |
| 113 # if that timestamp isn't as recent as what's already on disk, the .app will |
| 114 # need to be touched. |
| 115 NEEDS_TOUCH= |
| 116 if [ "${DEST}" -nt "${SRC}" ] ; then |
| 117 NEEDS_TOUCH=1 |
| 118 fi |
| 119 |
| 120 # Copy the unversioned files into place, leaving everything in |
| 121 # Contents/Versions alone. If this step is interrupted, the application will |
| 122 # at least remain in a usable state, although it may not pass signature |
| 123 # validation. Depending on when this step is interrupted, the application |
| 124 # will either launch the old or the new version. The critical point is when |
| 125 # the main executable is replaced. There isn't very much to copy in this step, |
| 126 # because most of the application is in the versioned directory. This step |
| 127 # only accounts for around 50 files, most of which are small localized |
| 128 # InfoPlist.strings files. |
| 129 rsync ${RSYNC_FLAGS} --delete-after --exclude /Contents/Versions \ |
| 130 "${SRC}/" "${DEST}" || exit 6 |
| 131 |
| 132 # If necessary, touch the outermost .app so that it appears to the outside |
| 133 # world that something was done to the bundle. This will cause LaunchServices |
| 134 # to invalidate the information it has cached about the bundle even if |
| 135 # lsregister does not run. This is not done if rsync already updated the |
| 136 # timestamp to something newer than what had been on disk. This is not |
| 137 # considered a critical step, and if it fails, this script will not exit. |
| 138 if [ -n "${NEEDS_TOUCH}" ] ; then |
| 139 touch -cf "${DEST}" || true |
| 140 fi |
| 78 | 141 |
| 79 # Read the new values (e.g. version). Get the installed application version | 142 # Read the new values (e.g. version). Get the installed application version |
| 80 # to get the path to the framework, where the Keystone keys are stored. | 143 # to get the path to the framework, where the Keystone keys are stored. |
| 81 NEW_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" || | 144 NEW_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" || |
| 82 exit 6) | 145 exit 7) |
| 83 NEW_KS_PLIST="${DEST}/Contents/Versions/${NEW_VERSION_APP}/${FRAMEWORK_NAME}/Res
ources/Info" | 146 NEW_KS_PLIST="${DEST}/Contents/Versions/${NEW_VERSION_APP}/${FRAMEWORK_DIR}/Reso
urces/Info" |
| 84 NEW_VERSION_KS=$(defaults read "${NEW_KS_PLIST}" "${KS_VERSION_KEY}" || exit 6) | 147 NEW_VERSION_KS=$(defaults read "${NEW_KS_PLIST}" "${KS_VERSION_KEY}" || exit 7) |
| 85 URL=$(defaults read "${NEW_KS_PLIST}" KSUpdateURL || exit 6) | 148 URL=$(defaults read "${NEW_KS_PLIST}" KSUpdateURL || exit 7) |
| 86 # The channel ID is optional. Suppress stderr to prevent Keystone from seeing | 149 # The channel ID is optional. Suppress stderr to prevent Keystone from seeing |
| 87 # possible error output. | 150 # possible error output. |
| 88 CHANNEL_ID=$(defaults read "${NEW_KS_PLIST}" KSChannelID 2>/dev/null || true) | 151 CHANNEL_ID=$(defaults read "${NEW_KS_PLIST}" KSChannelID 2>/dev/null || true) |
| 89 | 152 |
| 90 # Compare old and new versions. If they are equal we failed somewhere. | 153 # Compare old and new versions. If they are equal we failed somewhere. |
| 91 if [ "${OLD_VERSION_KS}" = "${NEW_VERSION_KS}" ]; then | 154 if [ "${OLD_VERSION_KS}" = "${NEW_VERSION_KS}" ]; then |
| 92 exit 7 | 155 exit 8 |
| 93 fi | 156 fi |
| 94 | 157 |
| 95 # Notify LaunchServices. | 158 # Notify LaunchServices. This is not considered a critical step, and |
| 96 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.fram
ework/Support/lsregister "${DEST}" | 159 # lsregister's exit codes shouldn't be confused with this script's own. |
| 97 | 160 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.fram
ework/Support/lsregister "${DEST}" || true |
| 98 # Notify Keystone. Older versions of Keystone don't recognize --tag. If the | 161 |
| 99 # command with --tag fails, retry without it. In that case, Chrome will set | 162 # Notify Keystone. |
| 100 # the tag when it runs. | 163 KSADMIN_VERSION=$(ksadmin --ksadmin-version || true) |
| 101 # TODO: The version of Keystone picking up --tag will also include support for | 164 if [ -n "${KSADMIN_VERSION}" ] ; then |
| 102 # --ksdamin-version. At that point, we can check to see if ksadmin honors the | 165 # If ksadmin recognizes --ksadmin-version, it will recognize --tag. |
| 103 # version check; if not, no --tag, if yes, do a case...esac on the version | |
| 104 # patterns for any support checks we need. | |
| 105 ksadmin --register \ | |
| 106 -P "${PRODUCT_ID}" \ | |
| 107 --version "${NEW_VERSION_KS}" \ | |
| 108 --xcpath "${DEST}" \ | |
| 109 --url "${URL}" \ | |
| 110 --tag "${CHANNEL_ID}" || \ | |
| 111 ksadmin --register \ | 166 ksadmin --register \ |
| 112 -P "${PRODUCT_ID}" \ | 167 -P "${PRODUCT_ID}" \ |
| 113 --version "${NEW_VERSION_KS}" \ | 168 --version "${NEW_VERSION_KS}" \ |
| 114 --xcpath "${DEST}" \ | 169 --xcpath "${DEST}" \ |
| 115 --url "${URL}" || exit 8 | 170 --url "${URL}" \ |
| 171 --tag "${CHANNEL_ID}" || exit 9 |
| 172 else |
| 173 # Older versions of ksadmin don't recognize --tag. The application will |
| 174 # set the tag when it runs. |
| 175 ksadmin --register \ |
| 176 -P "${PRODUCT_ID}" \ |
| 177 --version "${NEW_VERSION_KS}" \ |
| 178 --xcpath "${DEST}" \ |
| 179 --url "${URL}" || exit 9 |
| 180 fi |
| 181 |
| 182 # The remaining steps are not considered critical. |
| 183 set +e |
| 184 |
| 185 # Try to clean up old versions that are not in use. The strategy is to keep |
| 186 # the versioned directory corresponding to the update just applied |
| 187 # (obviously) and the version that was just replaced, and to use ps and lsof |
| 188 # to see if it looks like any processes are currently using any other old |
| 189 # directories. Directories not in use are removed. Old versioned directories |
| 190 # that are in use are left alone so as to not interfere with running |
| 191 # processes. These directories can be cleaned up by this script on future |
| 192 # updates. |
| 193 # |
| 194 # To determine which directories are in use, both ps and lsof are used. Each |
| 195 # approach has limitations. |
| 196 # |
| 197 # The ps check looks for processes within the verisoned directory. Only |
| 198 # helper processes, such as renderers, are within the versioned directory. |
| 199 # Browser processes are not, so the ps check will not find them, and will |
| 200 # assume that a versioned directory is not in use if a browser is open without |
| 201 # any windows. The ps mechanism can also only detect processes running on the |
| 202 # system that is performing the update. If network shares are involved, all |
| 203 # bets are off. |
| 204 # |
| 205 # The lsof check looks to see what processes have the framework dylib open. |
| 206 # Browser processes will have their versioned framework dylib open, so this |
| 207 # check is able to catch browsers even if there are no associated helper |
| 208 # processes. Like the ps check, the lsof check is limited to processes on |
| 209 # the system that is performing the update. Finally, unless running as root, |
| 210 # the lsof check can only find processes running as the effective user |
| 211 # performing the update. |
| 212 # |
| 213 # These limitations are motiviations to additionally preserve the versioned |
| 214 # directory corresponding to the version that was just replaced. |
| 215 |
| 216 # Set the nullglob option. This causes a glob pattern that doesn't match |
| 217 # any files to expand to an empty string, instead of expanding to the glob |
| 218 # pattern itself. This means that if /path/* doesn't match anything, it will |
| 219 # expand to "" instead of, literally, "/path/*". The glob used in the loop |
| 220 # below is not expected to expand to nothing, but nullglob will prevent the |
| 221 # loop from trying to remove nonexistent directories by weird names with |
| 222 # funny characters in them. |
| 223 shopt -s nullglob |
| 224 |
| 225 for versioned_dir in "${DEST}/Contents/Versions/"* ; do |
| 226 if [ "${versioned_dir}" = "${NEW_VERSIONED_DIR}" ] || \ |
| 227 [ "${versioned_dir}" = "${OLD_VERSIONED_DIR}" ] ; then |
| 228 # This is the versioned directory corresponding to the update that was |
| 229 # just applied or the version that was previously in use. Leave it alone. |
| 230 continue |
| 231 fi |
| 232 |
| 233 # Look for any processes whose executables are within this versioned |
| 234 # directory. They'll be helper processes, such as renderers. Their |
| 235 # existence indicates that this versioned directory is currently in use. |
| 236 PS_STRING="${versioned_dir}/" |
| 237 |
| 238 # Look for any processes using the framework dylib. This will catch |
| 239 # browser processes where the ps check will not, but it is limited to |
| 240 # processes running as the effective user. |
| 241 LSOF_FILE="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}" |
| 242 |
| 243 # ps -e displays all users' processes, -ww causes ps to not truncate lines, |
| 244 # -o comm instructs it to only print the command name, and the = tells it to |
| 245 # not print a header line. |
| 246 # The cut invocation filters the ps output to only have at most the number |
| 247 # of characters in ${PS_STRING}. This is done so that grep can look for an |
| 248 # exact match. |
| 249 # grep -F tells grep to look for lines that are exact matches (not regular |
| 250 # expressions), -q tells it to not print any output and just indicate |
| 251 # matches by exit status, and -x tells it that the entire line must match |
| 252 # ${PS_STRING} exactly, as opposed to matching a substring. A match |
| 253 # causes grep to exit zero (true). |
| 254 # |
| 255 # lsof will exit nonzero if ${LSOF_FILE} does not exist or is open by any |
| 256 # process. If the file exists and is open, it will exit zero (true). |
| 257 if (! ps -ewwo comm= | \ |
| 258 cut -c "1-${#PS_STRING}" | \ |
| 259 grep -Fqx "${PS_STRING}") && |
| 260 (! lsof "${LSOF_FILE}" >& /dev/null) ; then |
| 261 # It doesn't look like anything is using this versioned directory. Get rid |
| 262 # of it. |
| 263 rm -rf "${versioned_dir}" |
| 264 fi |
| 265 done |
| 116 | 266 |
| 117 # Great success! | 267 # Great success! |
| 118 exit 0 | 268 exit 0 |
| OLD | NEW |