blob: f2e85453c1537da1485f1953658c7fa82b0382ae [file] [log] [blame]
#!/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 ${?}