| #!/bin/bash -p |
| # Copyright 2020 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # usage: install.sh update_dmg_mount_point installed_app_path current_version |
| # |
| # Called by Omaha v4 to update the installed application with a new version from |
| # the dmg. |
| # |
| # Exit codes: |
| # 0 Success! |
| # 1 Unknown Failure |
| # 2 Could not locate installed app path. |
| # 3 DMG mount point is not an absolute path. |
| # 4 Path to new .app must be an absolute path. |
| # 5 Update app is not using the new version folder layout. |
| # 6 Installed app path must be an absolute path to a directory. |
| # 7 Installed app's versioned directory is in old format. |
| # 8 Installed app's versioned directory is in old format. |
| # 9 Installed app's versioned directory is in old format. |
| # 10 Making versioned directory for new version failed. |
| # 11 Could not remove existing file where versioned directory should be. |
| # 12 rsync of versioned directory failed. |
| # 13 rsync of app directory failed. |
| # 14 We could not determine the new app version. |
| # 15 The new app version does not match the update version. |
| # 16 This will return a usage message. |
| |
| set -eu |
| |
| # Set path to /bin, /usr/bin, /sbin, /usr/sbin |
| export PATH="/bin:/usr/bin:/sbin:/usr/sbin" |
| |
| # Environment sanitization. Clear environment variables that might impact the |
| # interpreter's operation. The |bash -p| invocation on the #! line takes the |
| # bite out of BASH_ENV, ENV, and SHELLOPTS (among other features), but |
| # clearing them here ensures that they won't impact any shell scripts used as |
| # utility programs. SHELLOPTS is read-only and can't be unset, only |
| # unexported. |
| unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT |
| export -n SHELLOPTS |
| |
| set -o pipefail |
| shopt -s nullglob |
| |
| ME="$(basename "${0}")" |
| readonly ME |
| |
| # We will populate this variable with the version of the application bundle |
| # it'll be packaged with when the build is run. This allows us to not do a |
| # defaults read on Info.plist to find the update_version. |
| UPDATE_VERSION= |
| readonly UPDATE_VERSION |
| |
| err() { |
| local error="${1}" |
| local id=": ${$} $(date "+%Y-%m-%d %H:%M:%S %z")" |
| echo "${ME}${id}: ${error}" >& 2 |
| } |
| |
| note() { |
| local message="${1}" |
| echo "${ME}: ${$} $(date "+%Y-%m-%d %H:%M:%S %z"): ${message}" >& 1 |
| } |
| |
| g_temp_dir= |
| cleanup() { |
| local status=${?} |
| |
| trap - EXIT |
| trap '' HUP INT QUIT TERM |
| |
| if [[ ${status} -ge 128 ]]; then |
| err "Caught signal $((${status} - 128))" |
| fi |
| |
| if [[ -n "${g_temp_dir}" ]]; then |
| rm -rf "${g_temp_dir}" |
| fi |
| |
| exit ${status} |
| } |
| |
| # Returns 0 (true) if |symlink| exists, is a symbolic link, and appears |
| # writable on the basis of its POSIX permissions. This is used to determine |
| # writability like test's -w primary, but -w resolves symbolic links and this |
| # function does not. |
| is_writable_symlink() { |
| local symlink="${1}" |
| |
| local link_mode="$(stat -f %Sp "${symlink}" 2> /dev/null || true)" |
| if [[ -z "${link_mode}" ]] || [[ "${link_mode:0:1}" != "l" ]]; then |
| return 1 |
| fi |
| |
| local link_user="$(stat -f %u "${symlink}" 2> /dev/null || true)" |
| local link_group="$(stat -f %g "${symlink}" 2> /dev/null || true)" |
| if [[ -z "${link_user}" ]] || [[ -z "${link_group}" ]]; then |
| return 1 |
| fi |
| |
| # If the users match, check the owner-write bit. |
| if [[ ${EUID} -eq "${link_user}" ]]; then |
| if [[ "${link_mode:2:1}" = "w" ]]; then |
| return 0 |
| fi |
| return 1 |
| fi |
| |
| # If the file's group matches any of the groups that this process is a |
| # member of, check the group-write bit. |
| local group_match= |
| local group |
| for group in "${GROUPS[@]}"; do |
| if [[ "${group}" -eq "${link_group}" ]]; then |
| group_match="y" |
| break |
| fi |
| done |
| if [[ -n "${group_match}" ]]; then |
| if [[ "${link_mode:5:1}" = "w" ]]; then |
| return 0 |
| fi |
| return 1 |
| fi |
| |
| # Check the other-write bit. |
| if [[ "${link_mode:8:1}" = "w" ]]; then |
| return 0 |
| fi |
| |
| return 1 |
| } |
| |
| # If |symlink| exists and is a symbolic link, but is not writable according to |
| # is_writable_symlink, this function attempts to replace it with a new |
| # writable symbolic link. If |symlink| does not exist, is not a symbolic |
| # link, or is already writable, this function does nothing. This function |
| # always returns 0 (true). |
| ensure_writable_symlink() { |
| local symlink="${1}" |
| |
| if [[ -L "${symlink}" ]] && ! is_writable_symlink "${symlink}"; then |
| # If ${symlink} refers to a directory, doing this naively might result in |
| # the new link being placed in that directory, instead of replacing the |
| # existing link. ln -fhs is supposed to handle this case, but it does so |
| # by unlinking (removing) the existing symbolic link before creating a new |
| # one. That leaves a small window during which the symbolic link is not |
| # present on disk at all. |
| # |
| # To avoid that possibility, a new symbolic link is created in a temporary |
| # location and then swapped into place with mv. An extra temporary |
| # directory is used to convince mv to replace the symbolic link: again, if |
| # the existing link refers to a directory, "mv newlink oldlink" will |
| # actually leave oldlink alone and place newlink into the directory. |
| # "mv newlink dirname(oldlink)" works as expected, but in order to replace |
| # oldlink, newlink must have the same basename, hence the temporary |
| # directory. |
| |
| local target="$(readlink "${symlink}" 2> /dev/null || true)" |
| if [[ -z "${target}" ]]; then |
| return 0 |
| fi |
| |
| # Error handling strategy: if anything fails, such as the mktemp, ln, |
| # chmod, or mv, ignore the failure and return 0 (success), leaving the |
| # existing state with the non-writable symbolic link intact. Failures |
| # in this function will be difficult to understand and diagnose, and a |
| # non-writable symbolic link is not necessarily fatal. If something else |
| # requires a writable symbolic link, allowing it to fail when a symbolic |
| # link is not writable is easier to understand than bailing out of the |
| # script on failure here. |
| |
| local symlink_dir="$(dirname "${symlink}")" |
| local temp_link_dir=\ |
| "$(mktemp -d "${symlink_dir}/.symlink_temp.XXXXXX" || true)" |
| if [[ -z "${temp_link_dir}" ]]; then |
| return 0 |
| fi |
| local temp_link="${temp_link_dir}/$(basename "${symlink}")" |
| |
| (ln -fhs "${target}" "${temp_link}" && |
| chmod -h 755 "${temp_link}" && |
| mv -f "${temp_link}" "${symlink_dir}/") || true |
| rm -rf "${temp_link_dir}" |
| fi |
| |
| return 0 |
| } |
| |
| # ensure_writable_symlinks_recursive calls ensure_writable_symlink for every |
| # symbolic link in |directory|, recursively. |
| # |
| # In some very weird and rare cases, it is possible to wind up with a user |
| # installation that contains symbolic links that the user does not have write |
| # permission over. More on how that might happen later. |
| # |
| # If a weird and rare case like this is observed, rsync will exit with an |
| # error when attempting to update the times on these symbolic links. rsync |
| # may not be intelligent enough to try creating a new symbolic link in these |
| # cases, but this script can be. |
| # |
| # The problem occurs when an administrative user first drag-installs the |
| # application to /Applications, resulting in the program's user being set to |
| # the user's own ID. If, subsequently, a .pkg package is installed over that, |
| # the existing directory ownership will be preserved, but file ownership will |
| # be changed to whatever is specified by the package, typically root. This |
| # applies to symbolic links as well. On a subsequent update, rsync will be |
| # able to copy the new files into place, because the user still has permission |
| # to write to the directories. If the symbolic link targets are not changing, |
| # though, rsync will not replace them, and they will remain owned by root. |
| # The user will not have permission to update the time on the symbolic links, |
| # resulting in an rsync error. |
| ensure_writable_symlinks_recursive() { |
| local directory="${1}" |
| |
| # This fix-up is not necessary when running as root, because root will |
| # always be able to write everything needed. |
| if [[ ${EUID} -eq 0 ]]; then |
| return 0 |
| fi |
| |
| # This step isn't critical. |
| local set_e= |
| if [[ "${-}" =~ e ]]; then |
| set_e="y" |
| set +e |
| fi |
| |
| # Use find -print0 with read -d $'\0' to handle even the weirdest paths. |
| local symlink |
| while IFS= read -r -d $'\0' symlink; do |
| ensure_writable_symlink "${symlink}" |
| done < <(find "${directory}" -type l -print0) |
| |
| # Go back to how things were. |
| if [[ -n "${set_e}" ]]; then |
| set -e |
| fi |
| } |
| |
| # Runs "defaults read" to obtain the value of a key in a property list. As |
| # with "defaults read", an absolute path to a plist is supplied, without the |
| # ".plist" extension. |
| # |
| # As of Mac OS X 10.8, defaults (and NSUserDefaults and CFPreferences) |
| # normally communicates with cfprefsd to read and write plists. Changes to a |
| # plist file aren't necessarily reflected immediately via this API family when |
| # not made through this API family, because cfprefsd may return cached data |
| # from a former on-disk version of a plist file instead of reading the current |
| # version from disk. The old behavior can be restored by setting the |
| # __CFPREFERENCES_AVOID_DAEMON environment variable, although extreme care |
| # should be used because portions of the system that use this API family |
| # normally and thus use cfprefsd and its cache will become unsynchronized with |
| # the on-disk state. |
| # |
| # This function is provided to set __CFPREFERENCES_AVOID_DAEMON when calling |
| # "defaults read" and thus avoid cfprefsd and its on-disk cache, and is |
| # intended only to be used to read values from Info.plist files, which are not |
| # preferences. The use of "defaults" for this purpose has always been |
| # questionable, but there's no better option to interact with plists from |
| # shell scripts. Definitely don't use infoplist_read to read preference |
| # plists. |
| # |
| # This function exists because the update process delivers new copies of |
| # Info.plist files to the disk behind cfprefsd's back, and if cfprefsd becomes |
| # aware of the original version of the file for any reason (such as this |
| # script reading values from it via "defaults read"), the new version of the |
| # file will not be immediately effective or visible via cfprefsd after the |
| # update is applied. |
| infoplist_read() { |
| __CFPREFERENCES_AVOID_DAEMON=1 defaults read "${@}" |
| } |
| |
| usage() { |
| echo "usage: ${ME} update_dmg_mount_point installed_app_path current_version"\ |
| >& 2 |
| } |
| |
| main() { |
| local update_dmg_mount_point="${1}" |
| local installed_app_path="${2}" |
| local old_version_app="${3}" |
| |
| # Early steps are critical. Don't continue past any failure. |
| set -e |
| |
| trap cleanup EXIT HUP INT QUIT TERM |
| |
| # Figure out where to install. |
| if [[ ! -d "${installed_app_path}" ]]; then |
| err "couldn't locate installed_app_path" |
| exit 2 |
| fi |
| note "installed_app_path = ${installed_app_path}" |
| note "old_version_app = ${old_version_app}" |
| |
| # The app directory, product name, etc. can all be gotten from the |
| # installed_app_path. |
| readonly APP_DIR="$(basename "${installed_app_path}")" |
| readonly PRODUCT_NAME="${APP_DIR%.*}" |
| readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework" |
| readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" |
| readonly CONTENTS_DIR="Contents" |
| readonly APP_PLIST="${CONTENTS_DIR}/Info" |
| readonly VERSIONS_DIR_NEW=\ |
| "${CONTENTS_DIR}/Frameworks/${FRAMEWORK_DIR}/Versions" |
| readonly APP_VERSION_KEY="CFBundleShortVersionString" |
| |
| readonly QUARANTINE_ATTR="com.apple.quarantine" |
| |
| # Don't use rsync --archive, because --archive includes --group and --owner, |
| # which copy groups and owners, respectively, from the source, and that is |
| # undesirable in this case (often, this script will have permission to set |
| # those attributes). --archive also includes --devices and --specials, which |
| # copy files that should never occur in the transfer; --devices only works |
| # when running as root, so for consistency between privileged and unprivileged |
| # operation, this option is omitted as well. --archive does not include |
| # --ignore-times, which is desirable, as it forces rsync to copy files even |
| # when their sizes and modification times are identical, as their content |
| # still may be different. |
| readonly RSYNC_FLAGS="--ignore-times --links --perms --recursive --times" |
| |
| note "update_dmg_mount_point = ${update_dmg_mount_point}" |
| |
| if [[ -z "${update_dmg_mount_point}" ]] || |
| [[ "${update_dmg_mount_point:0:1}" != "/" ]] || |
| ! [[ -d "${update_dmg_mount_point}" ]]; then |
| err "update_dmg_mount_point must be an absolute path to a directory" |
| usage |
| exit 3 |
| fi |
| |
| # The update to install. |
| |
| # update_app is the path to the new version of the .app. |
| local update_app="${update_dmg_mount_point}/${APP_DIR}" |
| note "update_app = ${update_app}" |
| |
| # Make sure that it's an absolute path. |
| if [[ "${update_app:0:1}" != "/" ]]; then |
| err "update_app must be an absolute path" |
| exit 4 |
| fi |
| |
| if [[ ! -d "${update_app}/${VERSIONS_DIR_NEW}" ]]; then |
| err "update app not using new layout" |
| exit 5 |
| fi |
| |
| if [[ "${installed_app_path:0:1}" != "/" ]] || |
| ! [[ -d "${installed_app_path}" ]]; then |
| err "installed_app_path must be an absolute path to a directory" |
| exit 6 |
| fi |
| |
| # Figure out what the existing installed application is using for its |
| # versioned directory. This will be used later, to avoid removing the |
| # existing installed version's versioned directory in case anything is still |
| # using it. |
| note "reading install values" |
| |
| local installed_app_path_plist="${installed_app_path}/${APP_PLIST}" |
| note "installed_app_path_plist = ${installed_app_path_plist}" |
| |
| local installed_versions_dir_new="${installed_app_path}/${VERSIONS_DIR_NEW}" |
| note "installed_versions_dir_new = ${installed_versions_dir_new}" |
| |
| local installed_versions_dir="${installed_versions_dir_new}" |
| if [[ ! -d "${installed_versions_dir}" ]]; then |
| err "installed app does not have versioned dir. Might be an old version." |
| exit 7 |
| fi |
| note "installed_versions_dir = ${installed_versions_dir}" |
| |
| # If the installed application is incredibly old, or in a skeleton bootstrap |
| # installation, old_versioned_dir may not exist. |
| local old_versioned_dir |
| if [[ -n "${old_version_app}" ]]; then |
| if [[ -d "${installed_versions_dir_new}/${old_version_app}" ]]; then |
| old_versioned_dir="${installed_versions_dir_new}/${old_version_app}" |
| else |
| err "installed app does not have versioned dir. Might be an old version." |
| exit 8 |
| fi |
| fi |
| note "old_versioned_dir = ${old_versioned_dir}" |
| |
| local update_versioned_dir=\ |
| "${update_app}/${VERSIONS_DIR_NEW}/${UPDATE_VERSION}" |
| if [[ ! -d "${update_versioned_dir}" ]]; then |
| err "Update versioned dir does not have the new layout." |
| exit 9 |
| fi |
| |
| ensure_writable_symlinks_recursive "${installed_app_path}" |
| |
| # By copying to ${installed_app_path}, the existing application name will be |
| # preserved, if the user has renamed the application on disk. Respecting |
| # the user's changes is friendly. |
| |
| # Make sure that ${installed_versions_dir} exists, so that it can receive |
| # the versioned directory. It may not exist if updating from an older |
| # version that did not use the same versioned layout on disk. Later, during |
| # the rsync to copy the application directory, the mode bits and timestamp on |
| # ${installed_versions_dir} will be set to conform to whatever is present in |
| # the update. |
| # |
| # ${installed_app_path} is guaranteed to exist at this point, but |
| # ${installed_app_path}/${CONTENTS_DIR} may not if things are severely broken |
| # or if this update is actually an initial installation from an updater |
| # skeleton bootstrap. The mkdir creates ${installed_app_path}/${CONTENTS_DIR} |
| # if it doesn't exist; its mode bits will be fixed up in a subsequent rsync. |
| note "creating installed_versions_dir" |
| if ! mkdir -p "${installed_versions_dir}"; then |
| err "mkdir of installed_versions_dir failed" |
| exit 10 |
| fi |
| |
| local new_versioned_dir="${installed_versions_dir}/${UPDATE_VERSION}" |
| note "new_versioned_dir = ${new_versioned_dir}" |
| |
| # If there's an entry at ${new_versioned_dir} but it's not a directory |
| # (or it's a symbolic link, whether or not it points to a directory), rsync |
| # won't get rid of it. It's never correct to have a non-directory in place |
| # of the versioned directory, so toss out whatever's there. |
| if [[ -e "${new_versioned_dir}" ]] && |
| ([[ -L "${new_versioned_dir}" ]] || |
| ! [[ -d "${new_versioned_dir}" ]]); then |
| note "removing non-directory in place of versioned directory" |
| rm -f "${new_versioned_dir}" 2> /dev/null || true |
| |
| # If the non-directory new_versioned_dir still exists after we attempted to |
| # remove it, just fail early here, before we get to the rsync. |
| if [[ -e "${new_versioned_dir}" ]]; then |
| err "could not remove existing file where versioned directory should be" |
| exit 11 |
| fi |
| fi |
| |
| # Copy the versioned directory. The new versioned directory should have a |
| # different name than any existing one, so this won't harm anything already |
| # present in ${installed_versions_dir}, including the versioned directory |
| # being used by any running processes. If this step is interrupted, there |
| # will be an incomplete versioned directory left behind, but it won't |
| # interfere with anything, and it will be replaced or removed during a future |
| # update attempt. |
| # |
| # In certain cases, same-version updates are distributed to move users |
| # between channels; when this happens, the contents of the versioned |
| # directories are identical and rsync will not render the versioned |
| # directory unusable even for an instant. |
| if [[ -n "${update_versioned_dir}" ]]; then |
| note "rsyncing versioned directory" |
| if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ |
| "${new_versioned_dir}"; then |
| err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" |
| |
| # If the rsync of a new-layout versioned directory failed, remove it. |
| # The incomplete version would break code signature validation. |
| note "cleaning up new_versioned_dir" |
| rm -rf "${new_versioned_dir}" |
| exit 12 |
| fi |
| fi |
| |
| # See if the timestamp of what's currently on disk is newer than the |
| # update's outer .app's timestamp. rsync will copy the update's timestamp |
| # over, but if that timestamp isn't as recent as what's already on disk, the |
| # .app will need to be touched. |
| local needs_touch= |
| if [[ "${installed_app_path}" -nt "${update_app}" ]]; then |
| needs_touch="y" |
| fi |
| |
| # Copy the unversioned files into place, leaving everything in |
| # ${installed_versions_dir} alone. If this step is interrupted, the |
| # application will at least remain in a usable state, although it may not |
| # pass signature validation. Depending on when this step is interrupted, |
| # the application will either launch the old or the new version. The |
| # critical point is when the main executable is replaced. There isn't very |
| # much to copy in this step, because most of the application is in the |
| # versioned directory. This step only accounts for around 50 files, most of |
| # which are small localized InfoPlist.strings files. Note that |
| # ${VERSIONS_DIR_NEW} are included to copy their mode bits and timestamps, but |
| # their contents are excluded, having already been installed above. The |
| # ${VERSIONS_DIR_NEW}/Current symbolic link is updated or created in this |
| # step, however. |
| note "rsyncing app directory" |
| if ! rsync ${RSYNC_FLAGS} --delete-after \ |
| --include="/${VERSIONS_DIR_NEW}/Current" \ |
| --exclude="/${VERSIONS_DIR_NEW}/*" "${update_app}/" \ |
| "${installed_app_path}"; then |
| err "rsync of app directory failed, status ${PIPESTATUS[0]}" |
| exit 13 |
| fi |
| |
| note "rsyncs complete" |
| |
| if [[ -n "${g_temp_dir}" ]]; then |
| # The temporary directory, if any, is no longer needed. |
| rm -rf "${g_temp_dir}" 2> /dev/null || true |
| g_temp_dir= |
| note "g_temp_dir = ${g_temp_dir}" |
| fi |
| |
| # If necessary, touch the outermost .app so that it appears to the outside |
| # world that something was done to the bundle. This will cause |
| # LaunchServices to invalidate the information it has cached about the |
| # bundle even if lsregister does not run. This is not done if rsync already |
| # updated the timestamp to something newer than what had been on disk. This |
| # is not considered a critical step, and if it fails, this script will not |
| # exit. |
| if [[ -n "${needs_touch}" ]]; then |
| touch -cf "${installed_app_path}" || true |
| fi |
| |
| # Read the new values, such as the version. |
| note "reading new values" |
| |
| local new_version_app |
| if ! new_version_app="$(infoplist_read "${installed_app_path_plist}" \ |
| "${APP_VERSION_KEY}")" || |
| [[ -z "${new_version_app}" ]]; then |
| err "couldn't determine new_version_app" |
| exit 14 |
| fi |
| |
| local new_versioned_dir="${installed_versions_dir}/${new_version_app}" |
| |
| # Make sure that the update was successful by comparing the version found in |
| # the update with the version now on disk. |
| if [[ "${new_version_app}" != "${UPDATE_VERSION}" ]]; then |
| err "new_version_app and UPDATE_VERSION do not match" |
| exit 15 |
| fi |
| |
| # Notify LaunchServices. This is not considered a critical step, and |
| # lsregister's exit codes shouldn't be confused with this script's own. |
| # Redirect stdout to /dev/null to suppress the useless "ThrottleProcessIO: |
| # throttling disk i/o" messages that lsregister might print. |
| note "notifying LaunchServices" |
| local coreservices="/System/Library/Frameworks/CoreServices.framework" |
| local launchservices="${coreservices}/Frameworks/LaunchServices.framework" |
| local lsregister="${launchservices}/Support/lsregister" |
| "${lsregister}" -f "${installed_app_path}" > /dev/null || true |
| |
| # The remaining steps are not considered critical. |
| set +e |
| |
| # Try to clean up old versions that are not in use. The strategy is to keep |
| # the versioned directory corresponding to the update just applied |
| # (obviously) and the version that was just replaced, and to use ps and lsof |
| # to see if it looks like any processes are currently using any other old |
| # directories. Directories not in use are removed. Old versioned |
| # directories that are in use are left alone so as to not interfere with |
| # running processes. These directories can be cleaned up by this script on |
| # future updates. |
| # |
| # To determine which directories are in use, both ps and lsof are used. |
| # Each approach has limitations. |
| # |
| # The ps check looks for processes within the versioned directory. Only |
| # helper processes, such as renderers, are within the versioned directory. |
| # Browser processes are not, so the ps check will not find them, and will |
| # assume that a versioned directory is not in use if a browser is open |
| # without any windows. The ps mechanism can also only detect processes |
| # running on the system that is performing the update. If network shares |
| # are involved, all bets are off. |
| # |
| # The lsof check looks to see what processes have the framework dylib open. |
| # Browser processes will have their versioned framework dylib open, so this |
| # check is able to catch browsers even if there are no associated helper |
| # processes. Like the ps check, the lsof check is limited to processes on |
| # the system that is performing the update. Finally, unless running as |
| # root, the lsof check can only find processes running as the effective user |
| # performing the update. |
| # |
| # These limitations are motivations to additionally preserve the versioned |
| # directory corresponding to the version that was just replaced. |
| note "cleaning up old versioned directories" |
| |
| local versioned_dir |
| for versioned_dir in "${installed_versions_dir_new}/"*; do |
| note "versioned_dir = ${versioned_dir}" |
| if [[ "${versioned_dir}" = "${new_versioned_dir}" ]] || |
| [[ "${versioned_dir}" = "${old_versioned_dir}" ]] || |
| [[ "${versioned_dir}" = "${installed_versions_dir_new}/Current" ]]; then |
| # This is the versioned directory corresponding to the update that was |
| # just applied or the version that was previously in use. Leave it |
| # alone. |
| continue |
| fi |
| |
| # Look for any processes whose executables are within this versioned |
| # directory. They'll be helper processes, such as renderers. Their |
| # existence indicates that this versioned directory is currently in use. |
| local ps_string="${versioned_dir}/" |
| |
| # Look for any processes using the framework dylib. This will catch |
| # browser processes where the ps check will not, but it is limited to |
| # processes running as the effective user. |
| local lsof_file |
| if [[ -e "${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}" ]]; then |
| # Old layout. |
| lsof_file="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}" |
| else |
| # New layout. |
| lsof_file="${versioned_dir}/${FRAMEWORK_NAME}" |
| fi |
| |
| # ps -e displays all users' processes, -ww causes ps to not truncate |
| # lines, -o comm instructs it to only print the command name, and the = |
| # tells it to not print a header line. |
| # The cut invocation filters the ps output to only have at most the number |
| # of characters in ${ps_string}. This is done so that grep can look for |
| # an exact match. |
| # grep -F tells grep to look for lines that are exact matches (not regular |
| # expressions), -q tells it to not print any output and just indicate |
| # matches by exit status, and -x tells it that the entire line must match |
| # ${ps_string} exactly, as opposed to matching a substring. A match |
| # causes grep to exit zero (true). |
| # |
| # lsof will exit nonzero if ${lsof_file} does not exist or is open by any |
| # process. If the file exists and is open, it will exit zero (true). |
| if (! ps -ewwo comm= | \ |
| cut -c "1-${#ps_string}" | \ |
| grep -Fqx "${ps_string}") && |
| (! lsof "${lsof_file}" >& /dev/null); then |
| # It doesn't look like anything is using this versioned directory. Get |
| # rid of it. |
| note "versioned_dir doesn't appear to be in use, removing" |
| rm -rf "${versioned_dir}" |
| else |
| note "versioned_dir is in use, skipping" |
| fi |
| done |
| |
| note "setting permissions" |
| |
| local chmod_mode="a+rX,u+w,go-w" |
| if [[ "${installed_app_path:0:14}" = "/Applications/" ]] && |
| chgrp -Rh admin "${installed_app_path}" 2> /dev/null; then |
| chmod_mode="a+rX,ug+w,o-w" |
| else |
| chown -Rh root:wheel "${installed_app_path}" 2> /dev/null |
| fi |
| |
| note "chmod_mode = ${chmod_mode}" |
| chmod -R "${chmod_mode}" "${installed_app_path}" 2> /dev/null |
| |
| # On the Mac, or at least on HFS+, symbolic link permissions are significant, |
| # but chmod -R and -h can't be used together. Do another pass to fix the |
| # permissions on any symbolic links. |
| find "${installed_app_path}" -type l -exec chmod -h "${chmod_mode}" {} + \ |
| 2> /dev/null |
| |
| # If an update is triggered from within the application itself, the update |
| # process inherits the quarantine bit (LSFileQuarantineEnabled). Any files |
| # or directories created during the update will be quarantined in that case, |
| # which may cause Launch Services to display quarantine UI. That's bad, |
| # especially if it happens when the outer .app launches a quarantined inner |
| # helper. If the application is already on the system and is being updated, |
| # then it can be assumed that it should not be quarantined. Use xattr to |
| # drop the quarantine attribute. |
| # |
| # TODO(mark): Instead of letting the quarantine attribute be set and then |
| # dropping it here, figure out a way to get the update process to run |
| # without LSFileQuarantineEnabled even when triggering an update from within |
| # the application. |
| note "lifting quarantine" |
| |
| xattr -d -r "${QUARANTINE_ATTR}" "${installed_app_path}" 2> /dev/null |
| |
| # Great success! |
| note "done!" |
| |
| trap - EXIT |
| |
| return 0 |
| } |
| |
| # Check "less than" instead of "not equal to" in case there are changes to pass |
| # more arguments. |
| if [[ ${#} -lt 3 ]]; then |
| usage |
| echo ${#} >& 1 |
| exit 16 |
| fi |
| |
| main "${@}" |
| exit ${?} |