mirror of
https://github.com/mtan93/Installomator.git
synced 2026-03-08 05:31:53 +00:00
850 lines
32 KiB
Bash
850 lines
32 KiB
Bash
# MARK: Functions
|
|
|
|
cleanupAndExit() { # $1 = exit code, $2 message, $3 level
|
|
if [ -n "$dmgmount" ]; then
|
|
# unmount disk image
|
|
printlog "Unmounting $dmgmount" DEBUG
|
|
unmountingOut=$(hdiutil detach "$dmgmount" 2>&1)
|
|
printlog "Debugging enabled, Unmounting output was:\n$unmountingOut" DEBUG
|
|
fi
|
|
if [ "$DEBUG" -ne 1 ]; then
|
|
# remove the temporary working directory when done (only if DEBUG is not used)
|
|
printlog "Deleting $tmpDir" DEBUG
|
|
deleteTmpOut=$(rm -Rfv "$tmpDir")
|
|
printlog "Debugging enabled, Deleting tmpDir output was:\n$deleteTmpOut" DEBUG
|
|
fi
|
|
|
|
# If we closed any processes, reopen the app again
|
|
reopenClosedProcess
|
|
if [[ -n $2 && $1 -ne 0 ]]; then
|
|
printlog "ERROR: $2" $3
|
|
fi
|
|
printlog "################## End Installomator, exit code $1 \n\n" REQ
|
|
|
|
# if label is wrong and we wanted name of the label, then return ##################
|
|
if [[ $RETURN_LABEL_NAME -eq 1 ]]; then
|
|
1=0 # If only label name should be returned we exit without any errors
|
|
echo "#"
|
|
fi
|
|
exit "$1"
|
|
}
|
|
|
|
runAsUser() {
|
|
if [[ $currentUser != "loginwindow" ]]; then
|
|
uid=$(id -u "$currentUser")
|
|
launchctl asuser $uid sudo -u $currentUser "$@"
|
|
fi
|
|
}
|
|
|
|
reloadAsUser() {
|
|
if [[ $currentUser != "loginwindow" ]]; then
|
|
uid=$(id -u "$currentUser")
|
|
su - $currentUser -c "${@}"
|
|
fi
|
|
}
|
|
|
|
displaydialog() { # $1: message $2: title
|
|
message=${1:-"Message"}
|
|
title=${2:-"Installomator"}
|
|
runAsUser osascript -e "button returned of (display dialog \"$message\" with title \"$title\" buttons {\"Not Now\", \"Quit and Update\"} default button \"Quit and Update\" with icon POSIX file \"$LOGO\")"
|
|
}
|
|
|
|
displaydialogContinue() { # $1: message $2: title
|
|
message=${1:-"Message"}
|
|
title=${2:-"Installomator"}
|
|
runAsUser osascript -e "button returned of (display dialog \"$message\" with title \"$title\" buttons {\"Quit and Update\"} default button \"Quit and Update\" with icon POSIX file \"$LOGO\")"
|
|
}
|
|
|
|
displaynotification() { # $1: message $2: title
|
|
message=${1:-"Message"}
|
|
title=${2:-"Notification"}
|
|
manageaction="/Library/Application Support/JAMF/bin/Management Action.app/Contents/MacOS/Management Action"
|
|
|
|
if [[ -x "$manageaction" ]]; then
|
|
"$manageaction" -message "$message" -title "$title"
|
|
else
|
|
runAsUser osascript -e "display notification \"$message\" with title \"$title\""
|
|
fi
|
|
}
|
|
|
|
|
|
# MARK: Logging
|
|
log_location="/private/var/log/Installomator.log"
|
|
|
|
# Check if we're in debug mode, if so then set logging to DEBUG, otherwise default to INFO
|
|
# if no log level is specified.
|
|
if [[ $DEBUG -ne 0 ]]; then
|
|
LOGGING=DEBUG
|
|
elif [[ -z $LOGGING ]]; then
|
|
LOGGING=INFO
|
|
datadogLoggingLevel=INFO
|
|
fi
|
|
|
|
# Associate logging levels with a numerical value so that we are able to identify what
|
|
# should be removed. For example if the LOGGING=ERROR only printlog statements with the
|
|
# level REQ and ERROR will be displayed. LOGGING=DEBUG will show all printlog statements.
|
|
# If a printlog statement has no level set it's automatically assigned INFO.
|
|
|
|
declare -A levels=(DEBUG 0 INFO 1 WARN 2 ERROR 3 REQ 4)
|
|
|
|
# If we are able to detect an MDM URL (Jamf Pro) or another identifier for a customer/instance we grab it here, this is useful if we're centrally logging multiple MDM instances.
|
|
if [[ -f /Library/Preferences/com.jamfsoftware.jamf.plist ]]; then
|
|
mdmURL=$(defaults read /Library/Preferences/com.jamfsoftware.jamf.plist jss_url)
|
|
elif [[ -n "$MDMProfileName" ]]; then
|
|
mdmURL=$(sudo profiles show | grep -A3 "$MDMProfileName" | sed -n -e 's/^.*organization: //p')
|
|
else
|
|
mdmURL="Unknown"
|
|
fi
|
|
|
|
# Generate a session key for this run, this is useful to idenify streams when we're centrally logging.
|
|
SESSION=$RANDOM
|
|
|
|
printlog(){
|
|
[ -z "$2" ] && 2=INFO
|
|
log_message=$1
|
|
log_priority=$2
|
|
timestamp=$(date +%F\ %T)
|
|
|
|
# Check to make sure that the log isn't the same as the last, if it is then don't log and increment a timer.
|
|
if [[ ${log_message} == ${previous_log_message} ]];then
|
|
let logrepeat=$logrepeat+1
|
|
return
|
|
fi
|
|
|
|
previous_log_message=$log_message
|
|
|
|
# Once we finally stop getting duplicate logs output the number of times we got a duplicate.
|
|
if [[ $logrepeat -gt 1 ]];then
|
|
echo "$timestamp" : "${log_priority} : $label : Last Log repeated ${logrepeat} times" | tee -a $log_location
|
|
|
|
if [[ ! -z $datadogAPI ]]; then
|
|
curl -s -X POST https://http-intake.logs.datadoghq.com/v1/input -H "Content-Type: text/plain" -H "DD-API-KEY: $datadogAPI" -d "${log_priority} : $mdmURL : $APPLICATION : $VERSION : $SESSION : Last Log repeated ${logrepeat} times" > /dev/null
|
|
fi
|
|
logrepeat=0
|
|
fi
|
|
|
|
# If the datadogAPI key value is set and our logging level is greaterthan or equal to our set level
|
|
# then post to Datadog's HTTPs endpoint.
|
|
if [[ -n $datadogAPI && ${levels[$log_priority]} -ge ${levels[$datadogLoggingLevel]} ]]; then
|
|
while IFS= read -r logmessage; do
|
|
curl -s -X POST https://http-intake.logs.datadoghq.com/v1/input -H "Content-Type: text/plain" -H "DD-API-KEY: $datadogAPI" -d "${log_priority} : $mdmURL : Installomator-${label} : ${VERSIONDATE//-/} : $SESSION : ${logmessage}" > /dev/null
|
|
done <<< "$log_message"
|
|
fi
|
|
|
|
# If our logging level is greaterthan or equal to our set level then output locally.
|
|
if [[ ${levels[$log_priority]} -ge ${levels[$LOGGING]} ]]; then
|
|
while IFS= read -r logmessage; do
|
|
if [[ "$(whoami)" == "root" ]]; then
|
|
echo "$timestamp" : "${log_priority} : $label : ${logmessage}" | tee -a $log_location
|
|
else
|
|
echo "$timestamp" : "${log_priority} : $label : ${logmessage}"
|
|
fi
|
|
done <<< "$log_message"
|
|
fi
|
|
}
|
|
|
|
# Used to remove dupplicate lines in large log output, for example from msupdate command
|
|
# after it finishes running.
|
|
deduplicatelogs() {
|
|
loginput=${1:-"Log"}
|
|
logoutput=""
|
|
# Read each line of the incoming log individually, match it with the previous.
|
|
# If it matches increment logrepeate then skip to the next line.
|
|
while read log; do
|
|
if [[ $log == $previous_log ]];then
|
|
let logrepeat=$logrepeat+1
|
|
continue
|
|
fi
|
|
|
|
previous_log="$log"
|
|
if [[ $logrepeat -gt 1 ]];then
|
|
logoutput+="Last Log repeated ${logrepeat} times\n"
|
|
logrepeat=0
|
|
fi
|
|
|
|
logoutput+="$log\n"
|
|
done <<< "$loginput"
|
|
}
|
|
|
|
# will get the latest release download from a github repo
|
|
downloadURLFromGit() { # $1 git user name, $2 git repo name
|
|
gitusername=${1?:"no git user name"}
|
|
gitreponame=${2?:"no git repo name"}
|
|
|
|
if [[ $type == "pkgInDmg" ]]; then
|
|
filetype="dmg"
|
|
elif [[ $type == "pkgInZip" ]]; then
|
|
filetype="zip"
|
|
else
|
|
filetype=$type
|
|
fi
|
|
|
|
if [ -n "$archiveName" ]; then
|
|
downloadURL=$(curl --silent --fail "https://api.github.com/repos/$gitusername/$gitreponame/releases/latest" \
|
|
| awk -F '"' "/browser_download_url/ && /$archiveName\"/ { print \$4; exit }")
|
|
else
|
|
downloadURL=$(curl --silent --fail "https://api.github.com/repos/$gitusername/$gitreponame/releases/latest" \
|
|
| awk -F '"' "/browser_download_url/ && /$filetype\"/ { print \$4; exit }")
|
|
fi
|
|
if [ -z "$downloadURL" ]; then
|
|
cleanupAndExit 9 "could not retrieve download URL for $gitusername/$gitreponame" ERROR
|
|
else
|
|
echo "$downloadURL"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
versionFromGit() {
|
|
# credit: Søren Theilgaard (@theilgaard)
|
|
# $1 git user name, $2 git repo name
|
|
gitusername=${1?:"no git user name"}
|
|
gitreponame=${2?:"no git repo name"}
|
|
|
|
appNewVersion=$(curl --silent --fail "https://api.github.com/repos/$gitusername/$gitreponame/releases/latest" | grep tag_name | cut -d '"' -f 4 | sed 's/[^0-9\.]//g')
|
|
if [ -z "$appNewVersion" ]; then
|
|
printlog "could not retrieve version number for $gitusername/$gitreponame"
|
|
appNewVersion=""
|
|
else
|
|
echo "$appNewVersion"
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
|
|
# Handling of differences in xpath between Catalina and Big Sur
|
|
xpath() {
|
|
# the xpath tool changes in Big Sur and now requires the `-e` option
|
|
if [[ $(sw_vers -buildVersion) > "20A" ]]; then
|
|
/usr/bin/xpath -e $@
|
|
# alternative: switch to xmllint (which is not perl)
|
|
#xmllint --xpath $@ -
|
|
else
|
|
/usr/bin/xpath $@
|
|
fi
|
|
}
|
|
|
|
|
|
getAppVersion() {
|
|
# modified by: Søren Theilgaard (@theilgaard) and Isaac Ordonez
|
|
|
|
# If label contain function appCustomVersion, we use that and return
|
|
if type 'appCustomVersion' 2>/dev/null | grep -q 'function'; then
|
|
appversion=$(appCustomVersion)
|
|
printlog "Custom App Version detection is used, found $appversion"
|
|
return
|
|
fi
|
|
|
|
# pkgs contains a version number, then we don't have to search for an app
|
|
if [[ $packageID != "" ]]; then
|
|
appversion="$(pkgutil --pkg-info-plist ${packageID} 2>/dev/null | grep -A 1 pkg-version | tail -1 | sed -E 's/.*>([0-9.]*)<.*/\1/g')"
|
|
if [[ $appversion != "" ]]; then
|
|
printlog "found packageID $packageID installed, version $appversion"
|
|
updateDetected="YES"
|
|
return
|
|
else
|
|
printlog "No version found using packageID $packageID"
|
|
fi
|
|
fi
|
|
|
|
# get app in /Applications, or /Applications/Utilities, or find using Spotlight
|
|
if [[ -d "/Applications/$appName" ]]; then
|
|
applist="/Applications/$appName"
|
|
elif [[ -d "/Applications/Utilities/$appName" ]]; then
|
|
applist="/Applications/Utilities/$appName"
|
|
else
|
|
applist=$(mdfind "kind:application $appName" -0 )
|
|
fi
|
|
if [[ -z applist ]]; then
|
|
printlog "No previous app found" DEBUG
|
|
else
|
|
printlog "App(s) found: ${applist}" DEBUG
|
|
fi
|
|
|
|
appPathArray=( ${(0)applist} )
|
|
|
|
if [[ ${#appPathArray} -gt 0 ]]; then
|
|
filteredAppPaths=( ${(M)appPathArray:#${targetDir}*} )
|
|
if [[ ${#filteredAppPaths} -eq 1 ]]; then
|
|
installedAppPath=$filteredAppPaths[1]
|
|
#appversion=$(mdls -name kMDItemVersion -raw $installedAppPath )
|
|
appversion=$(defaults read $installedAppPath/Contents/Info.plist $versionKey) #Not dependant on Spotlight indexing
|
|
printlog "found app at $installedAppPath, version $appversion, on versionKey $versionKey"
|
|
updateDetected="YES"
|
|
# Is current app from App Store
|
|
if [[ -d "$installedAppPath"/Contents/_MASReceipt ]];then
|
|
printlog "Installed $appName is from App Store, use “IGNORE_APP_STORE_APPS=yes” to replace."
|
|
if [[ $IGNORE_APP_STORE_APPS == "yes" ]]; then
|
|
printlog "Replacing App Store apps, no matter the version"
|
|
appversion=0
|
|
else
|
|
cleanupAndExit 1 "App previously installed from App Store, and we respect that" ERROR
|
|
fi
|
|
fi
|
|
else
|
|
printlog "could not determine location of $appName"
|
|
fi
|
|
else
|
|
printlog "could not find $appName"
|
|
fi
|
|
}
|
|
|
|
checkRunningProcesses() {
|
|
# don't check in DEBUG mode 1
|
|
if [[ $DEBUG -eq 1 ]]; then
|
|
printlog "DEBUG mode 1, not checking for blocking processes" DEBUG
|
|
return
|
|
fi
|
|
|
|
# try at most 3 times
|
|
for i in {1..4}; do
|
|
countedProcesses=0
|
|
for x in ${blockingProcesses}; do
|
|
if pgrep -xq "$x"; then
|
|
printlog "found blocking process $x"
|
|
appClosed=1
|
|
|
|
case $BLOCKING_PROCESS_ACTION in
|
|
quit|quit_kill)
|
|
printlog "telling app $x to quit"
|
|
runAsUser osascript -e "tell app \"$x\" to quit"
|
|
if [[ $i > 2 && $BLOCKING_PROCESS_ACTION = "quit_kill" ]]; then
|
|
printlog "Changing BLOCKING_PROCESS_ACTION to kill"
|
|
BLOCKING_PROCESS_ACTION=kill
|
|
else
|
|
# give the user a bit of time to quit apps
|
|
printlog "waiting 30 seconds for processes to quit"
|
|
sleep 30
|
|
fi
|
|
;;
|
|
kill)
|
|
printlog "killing process $x"
|
|
pkill $x
|
|
sleep 5
|
|
;;
|
|
prompt_user|prompt_user_then_kill)
|
|
button=$(displaydialog "Quit “$x” to continue updating? (Leave this dialogue if you want to activate this update later)." "The application “$x” needs to be updated.")
|
|
if [[ $button = "Not Now" ]]; then
|
|
cleanupAndExit 10 "user aborted update" ERROR
|
|
else
|
|
if [[ $i > 2 && $BLOCKING_PROCESS_ACTION = "prompt_user_then_kill" ]]; then
|
|
printlog "Changing BLOCKING_PROCESS_ACTION to kill"
|
|
BLOCKING_PROCESS_ACTION=kill
|
|
else
|
|
printlog "telling app $x to quit"
|
|
runAsUser osascript -e "tell app \"$x\" to quit"
|
|
# give the user a bit of time to quit apps
|
|
printlog "waiting 30 seconds for processes to quit"
|
|
sleep 30
|
|
fi
|
|
fi
|
|
;;
|
|
prompt_user_loop)
|
|
button=$(displaydialog "Quit “$x” to continue updating? (Click “Not Now” to be asked in 1 hour, or leave this open until you are ready)." "The application “$x” needs to be updated.")
|
|
if [[ $button = "Not Now" ]]; then
|
|
if [[ $i < 2 ]]; then
|
|
printlog "user wants to wait an hour"
|
|
sleep 3600 # 3600 seconds is an hour
|
|
else
|
|
printlog "change of BLOCKING_PROCESS_ACTION to tell_user"
|
|
BLOCKING_PROCESS_ACTION=tell_user
|
|
fi
|
|
else
|
|
printlog "telling app $x to quit"
|
|
runAsUser osascript -e "tell app \"$x\" to quit"
|
|
# give the user a bit of time to quit apps
|
|
printlog "waiting 30 seconds for processes to quit"
|
|
sleep 30
|
|
fi
|
|
;;
|
|
tell_user|tell_user_then_kill)
|
|
button=$(displaydialogContinue "Quit “$x” to continue updating? (This is an important update). Wait for notification of update before launching app again." "The application “$x” needs to be updated.")
|
|
printlog "telling app $x to quit"
|
|
runAsUser osascript -e "tell app \"$x\" to quit"
|
|
# give the user a bit of time to quit apps
|
|
printlog "waiting 30 seconds for processes to quit"
|
|
sleep 30
|
|
if [[ $i > 1 && $BLOCKING_PROCESS_ACTION = tell_user_then_kill ]]; then
|
|
printlog "Changing BLOCKING_PROCESS_ACTION to kill"
|
|
BLOCKING_PROCESS_ACTION=kill
|
|
fi
|
|
;;
|
|
silent_fail)
|
|
cleanupAndExit 12 "blocking process '$x' found, aborting" ERROR
|
|
;;
|
|
esac
|
|
|
|
countedProcesses=$((countedProcesses + 1))
|
|
fi
|
|
done
|
|
|
|
done
|
|
|
|
if [[ $countedProcesses -ne 0 ]]; then
|
|
cleanupAndExit 11 "could not quit all processes, aborting..." ERROR
|
|
fi
|
|
|
|
printlog "no more blocking processes, continue with update" REQ
|
|
}
|
|
|
|
reopenClosedProcess() {
|
|
# If Installomator closed any processes, let's get the app opened again
|
|
# credit: Søren Theilgaard (@theilgaard)
|
|
|
|
# don't reopen if REOPEN is not "yes"
|
|
if [[ $REOPEN != yes ]]; then
|
|
printlog "REOPEN=no, not reopening anything"
|
|
return
|
|
fi
|
|
|
|
# don't reopen in DEBUG mode 1
|
|
if [[ $DEBUG -eq 1 ]]; then
|
|
printlog "DEBUG mode 1, not reopening anything" DEBUG
|
|
return
|
|
fi
|
|
|
|
if [[ $appClosed == 1 ]]; then
|
|
printlog "Telling app $appName to open"
|
|
#runAsUser osascript -e "tell app \"$appName\" to open"
|
|
#runAsUser open -a "${appName}"
|
|
reloadAsUser "open -a \"${appName}\""
|
|
#reloadAsUser "open \"${(0)applist}\""
|
|
processuser=$(ps aux | grep -i "${appName}" | grep -vi "grep" | awk '{print $1}')
|
|
printlog "Reopened ${appName} as $processuser"
|
|
else
|
|
printlog "App not closed, so no reopen." DEBUG
|
|
fi
|
|
}
|
|
|
|
installAppWithPath() { # $1: path to app to install in $targetDir
|
|
# modified by: Søren Theilgaard (@theilgaard)
|
|
appPath=${1?:"no path to app"}
|
|
|
|
# check if app exists
|
|
if [ ! -e "$appPath" ]; then
|
|
cleanupAndExit 8 "could not find: $appPath" DEBUG
|
|
fi
|
|
|
|
# verify with spctl
|
|
printlog "Verifying: $appPath" INFO
|
|
appVerify=$(spctl -a -vv "$appPath" 2>&1 )
|
|
appVerifyStatus=$(echo $?)
|
|
teamID=$(echo $appVerify | awk '/origin=/ {print $NF }' | tr -d '()' )
|
|
deduplicatelogs "$appVerify"
|
|
|
|
if [[ $appVerifyStatus -ne 0 ]] ; then
|
|
#if ! teamID=$(spctl -a -vv "$appPath" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()' ); then
|
|
cleanupAndExit 4 "Error verifying $appPath error:\n$logoutput" ERROR
|
|
fi
|
|
printlog "Debugging enabled, App Verification output was:\n$logoutput" DEBUG
|
|
printlog "Team ID matching: $teamID (expected: $expectedTeamID )" INFO
|
|
|
|
if [ "$expectedTeamID" != "$teamID" ]; then
|
|
cleanupAndExit 5 "Team IDs do not match" ERROR
|
|
fi
|
|
|
|
# app versioncheck
|
|
appNewVersion=$(defaults read $appPath/Contents/Info.plist $versionKey)
|
|
if [[ -n $appNewVersion && $appversion == $appNewVersion ]]; then
|
|
printlog "Downloaded version of $name is $appNewVersion on versionKey $versionKey, same as installed."
|
|
if [[ $INSTALL != "force" ]]; then
|
|
message="$name, version $appNewVersion, is the latest version."
|
|
if [[ $currentUser != "loginwindow" && $NOTIFY == "all" ]]; then
|
|
printlog "notifying"
|
|
displaynotification "$message" "No update for $name!"
|
|
fi
|
|
cleanupAndExit 0 "No new version to install" INFO
|
|
else
|
|
printlog "Using force to install anyway."
|
|
fi
|
|
else
|
|
printlog "Downloaded version of $name is $appNewVersion on versionKey $versionKey (replacing version $appversion)."
|
|
fi
|
|
|
|
# macOS versioncheck
|
|
minimumOSversion=$(defaults read $appPath/Contents/Info.plist LSMinimumSystemVersion 2>/dev/null )
|
|
if [[ -n $minimumOSversion && $minimumOSversion =~ '[0-9.]*' ]]; then
|
|
printlog "App has LSMinimumSystemVersion: $minimumOSversion"
|
|
if ! is-at-least $minimumOSversion $installedOSversion; then
|
|
printlog "App requires higher System Version than installed: $installedOSversion"
|
|
message="Cannot install $name, version $appNewVersion, as it is not compatible with the running system version."
|
|
if [[ $currentUser != "loginwindow" && $NOTIFY == "all" ]]; then
|
|
printlog "notifying"
|
|
displaynotification "$message" "Error updating $name!"
|
|
fi
|
|
cleanupAndExit 6 "Installed macOS is too old for this app." INFO
|
|
fi
|
|
fi
|
|
|
|
# skip install for DEBUG 1
|
|
if [ "$DEBUG" -eq 1 ]; then
|
|
printlog "DEBUG mode 1 enabled, skipping remove, copy and chown steps" DEBUG
|
|
return 0
|
|
fi
|
|
|
|
# skip install for DEBUG 2
|
|
if [ "$DEBUG" -eq 2 ]; then
|
|
printlog "DEBUG mode 2 enabled, not installing anything, exiting" DEBUG
|
|
cleanupAndExit 0
|
|
fi
|
|
|
|
# Test if variable CLIInstaller is set
|
|
if [[ -z $CLIInstaller ]]; then
|
|
|
|
# remove existing application
|
|
if [ -e "$targetDir/$appName" ]; then
|
|
printlog "Removing existing $targetDir/$appName" DEBUG
|
|
deleteAppOut=$(rm -Rfv "$targetDir/$appName" 2>&1)
|
|
printlog "Debugging enabled, App removing output was:\n$deleteAppOut" DEBUG
|
|
fi
|
|
|
|
# copy app to /Applications
|
|
printlog "Copy $appPath to $targetDir"
|
|
if ! ditto "$appPath" "$targetDir/$appName"; then
|
|
cleanupAndExit 7 "Error while copying" ERROR
|
|
fi
|
|
|
|
# set ownership to current user
|
|
if [[ "$currentUser" != "loginwindow" && $SYSTEMOWNER -ne 1 ]]; then
|
|
printlog "Changing owner to $currentUser"
|
|
chown -R "$currentUser" "$targetDir/$appName"
|
|
else
|
|
printlog "No user logged in or SYSTEMOWNER=1, setting owner to root:wheel"
|
|
chown -R root:wheel "$targetDir/$appName"
|
|
fi
|
|
|
|
elif [[ ! -z $CLIInstaller ]]; then
|
|
mountname=$(dirname $appPath)
|
|
printlog "CLIInstaller exists, running installer command $mountname/$CLIInstaller $CLIArguments" INFO
|
|
|
|
CLIoutput=$("$mountname/$CLIInstaller" "${CLIArguments[@]}" 2>&1)
|
|
CLIstatus=$(echo $?)
|
|
dedupliatelogs "$CLIoutput"
|
|
|
|
if [ $CLIstatus -ne 0 ] ; then
|
|
cleanupAndExit 3 "Error installing $mountname/$CLIInstaller $CLIArguments error:\n$logoutput" ERROR
|
|
else
|
|
printlog "Succesfully ran $mountname/$CLIInstaller $CLIArguments" INFO
|
|
fi
|
|
printlog "Debugging enabled, update tool output was:\n$logoutput" DEBUG
|
|
fi
|
|
|
|
}
|
|
|
|
mountDMG() {
|
|
# mount the dmg
|
|
printlog "Mounting $tmpDir/$archiveName"
|
|
# always pipe 'Y\n' in case the dmg requires an agreement
|
|
dmgmountOut=$(echo 'Y'$'\n' | hdiutil attach "$tmpDir/$archiveName" -nobrowse -readonly )
|
|
dmgmountStatus=$(echo $?)
|
|
dmgmount=$(echo $dmgmountOut | tail -n 1 | cut -c 54- )
|
|
printlog "dmgmountOut is $dmgmountOut" DEBUG
|
|
deduplicatelogs "$dmgmountOut"
|
|
|
|
if [[ $dmgmountStatus -ne 0 ]] ; then
|
|
#if ! dmgmount=$(echo 'Y'$'\n' | hdiutil attach "$tmpDir/$archiveName" -nobrowse -readonly | tail -n 1 | cut -c 54- ); then
|
|
cleanupAndExit 3 "Error mounting $tmpDir/$archiveName error:\n$logoutput" ERROR
|
|
fi
|
|
if [[ ! -e $dmgmount ]]; then
|
|
cleanupAndExit 3 "Error accessing mountpoint for $tmpDir/$archiveName error:\n$logoutput" ERROR
|
|
fi
|
|
printlog "Debugging enabled, installer output was:\n$logoutput" DEBUG
|
|
|
|
printlog "Mounted: $dmgmount" INFO
|
|
}
|
|
|
|
installFromDMG() {
|
|
mountDMG
|
|
installAppWithPath "$dmgmount/$appName"
|
|
}
|
|
|
|
installFromPKG() {
|
|
# verify with spctl
|
|
printlog "Verifying: $archiveName"
|
|
|
|
spctlOut=$(spctl -a -vv -t install "$archiveName" 2>&1 )
|
|
spctlStatus=$(echo $?)
|
|
printlog "spctlOut is $spctlOut" DEBUG
|
|
teamID=$(echo $spctlOut | awk -F '(' '/origin=/ {print $2 }' | tr -d '()' )
|
|
deduplicatelogs "$spctlOut"
|
|
|
|
if [[ $spctlStatus -ne 0 ]] ; then
|
|
#if ! spctlout=$(spctl -a -vv -t install "$archiveName" 2>&1 ); then
|
|
cleanupAndExit 4 "Error verifying $archiveName error:\n$logoutput" ERROR
|
|
fi
|
|
|
|
# Apple signed software has no teamID, grab entire origin instead
|
|
if [[ -z $teamID ]]; then
|
|
teamID=$(echo $spctlout | awk -F '=' '/origin=/ {print $NF }')
|
|
fi
|
|
|
|
printlog "Team ID: $teamID (expected: $expectedTeamID )"
|
|
|
|
if [ "$expectedTeamID" != "$teamID" ]; then
|
|
cleanupAndExit 5 "Team IDs do not match!" ERROR
|
|
fi
|
|
|
|
# Check version of pkg to be installed if packageID is set
|
|
if [[ $packageID != "" && $appversion != "" ]]; then
|
|
printlog "Checking package version."
|
|
baseArchiveName=$(basename $archiveName)
|
|
expandedPkg="$tmpDir/${baseArchiveName}_pkg"
|
|
pkgutil --expand "$archiveName" "$expandedPkg"
|
|
appNewVersion=$(cat "$expandedPkg"/Distribution | xpath 'string(//installer-gui-script/pkg-ref[@id][@version]/@version)' 2>/dev/null )
|
|
rm -r "$expandedPkg"
|
|
printlog "Downloaded package $packageID version $appNewVersion"
|
|
if [[ $appversion == $appNewVersion ]]; then
|
|
printlog "Downloaded version of $name is the same as installed."
|
|
if [[ $INSTALL != "force" ]]; then
|
|
message="$name, version $appNewVersion, is the latest version."
|
|
if [[ $currentUser != "loginwindow" && $NOTIFY == "all" ]]; then
|
|
printlog "notifying"
|
|
displaynotification "$message" "No update for $name!"
|
|
fi
|
|
cleanupAndExit 0 "No new version to install" INFO
|
|
else
|
|
printlog "Using force to install anyway."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# skip install for DEBUG 1
|
|
if [ "$DEBUG" -eq 1 ]; then
|
|
printlog "DEBUG enabled, skipping installation" DEBUG
|
|
return 0
|
|
fi
|
|
|
|
# skip install for DEBUG 2
|
|
if [ "$DEBUG" -eq 2 ]; then
|
|
cleanupAndExit 0 "DEBUG mode 2 enabled, exiting" DEBUG
|
|
fi
|
|
|
|
# install pkg
|
|
printlog "Installing $archiveName to $targetDir"
|
|
pkgInstall=$(installer -verbose -dumplog -pkg "$archiveName" -tgt "$targetDir" 2>&1)
|
|
pkgInstallStatus=$(echo $?)
|
|
sleep 1
|
|
pkgEndTime=$(date "+$LogDateFormat")
|
|
pkgInstall+=$(echo "Output of /var/log/install.log below this line.\n")
|
|
pkgInstall+=$(echo "----------------------------------------------------------\n")
|
|
pkgInstall+=$(awk -v "b=$starttime" -v "e=$pkgEndTime" -F ',' '$1 >= b && $1 <= e' /var/log/install.log)
|
|
deduplicatelogs "$pkgInstall"
|
|
|
|
if [[ $pkgInstallStatus -ne 0 ]] && [[ $logoutput == *"requires Rosetta 2"* ]] && [[ $rosetta2 == no ]]; then
|
|
printlog "Package requires Rosetta 2, Installing Rosetta 2 and Installing Package" INFO
|
|
/usr/sbin/softwareupdate --install-rosetta --agree-to-license
|
|
rosetta2=yes
|
|
installFromPKG
|
|
fi
|
|
|
|
if [ $pkginstallstatus -ne 0 ] ; then
|
|
#if ! installer -pkg "$archiveName" -tgt "$targetDir" ; then
|
|
cleanupAndExit 9 "Error installing $archiveName error:\n$logoutput" ERROR
|
|
fi
|
|
printlog "Debugging enabled, installer output was:\n$logoutput" DEBUG
|
|
}
|
|
|
|
installFromZIP() {
|
|
# unzip the archive
|
|
printlog "Unzipping $archiveName"
|
|
|
|
# tar -xf "$archiveName"
|
|
|
|
# note: when you expand a zip using tar in Mojave the expanded
|
|
# app will never pass the spctl check
|
|
|
|
# unzip -o -qq "$archiveName"
|
|
|
|
# note: githubdesktop fails spctl verification when expanded
|
|
# with unzip
|
|
|
|
ditto -x -k "$archiveName" "$tmpDir"
|
|
installAppWithPath "$tmpDir/$appName"
|
|
}
|
|
|
|
installFromTBZ() {
|
|
# unzip the archive
|
|
printlog "Unzipping $archiveName"
|
|
tar -xf "$archiveName"
|
|
installAppWithPath "$tmpDir/$appName"
|
|
}
|
|
|
|
installPkgInDmg() {
|
|
mountDMG
|
|
# locate pkg in dmg
|
|
if [[ -z $pkgName ]]; then
|
|
# find first file ending with 'pkg'
|
|
findfiles=$(find "$dmgmount" -iname "*.pkg" -maxdepth 1 )
|
|
filearray=( ${(f)findfiles} )
|
|
if [[ ${#filearray} -eq 0 ]]; then
|
|
cleanupAndExit 20 "couldn't find pkg in dmg $archiveName" ERROR
|
|
fi
|
|
archiveName="${filearray[1]}"
|
|
printlog "found pkg: $archiveName"
|
|
else
|
|
if ls "$tmpDir/$pkgName" ; then
|
|
archiveName="$tmpDir/$pkgName"
|
|
else
|
|
# try searching for pkg
|
|
findfiles=$(find "$tmpDir" -iname "$pkgName")
|
|
filearray=( ${(f)findfiles} )
|
|
if [[ ${#filearray} -eq 0 ]]; then
|
|
cleanupAndExit 20 "couldn't find pkg “$pkgName” in zip $archiveName" ERROR
|
|
fi
|
|
# it is now safe to overwrite archiveName for installFromPKG
|
|
archiveName="${filearray[1]}"
|
|
printlog "found pkg: $archiveName"
|
|
fi
|
|
fi
|
|
|
|
# installFromPkgs
|
|
installFromPKG
|
|
}
|
|
|
|
installPkgInZip() {
|
|
# unzip the archive
|
|
printlog "Unzipping $archiveName"
|
|
tar -xf "$archiveName"
|
|
|
|
# locate pkg in zip
|
|
if [[ -z $pkgName ]]; then
|
|
# find first file ending with 'pkg'
|
|
findfiles=$(find "$tmpDir" -iname "*.pkg" -maxdepth 2 )
|
|
filearray=( ${(f)findfiles} )
|
|
if [[ ${#filearray} -eq 0 ]]; then
|
|
cleanupAndExit 20 "couldn't find pkg in zip $archiveName" ERROR
|
|
fi
|
|
# it is now safe to overwrite archiveName for installFromPKG
|
|
archiveName="${filearray[1]}"
|
|
printlog "found pkg: $archiveName"
|
|
else
|
|
if ls "$tmpDir/$pkgName" ; then
|
|
archiveName="$tmpDir/$pkgName"
|
|
else
|
|
# try searching for pkg
|
|
findfiles=$(find "$tmpDir" -iname "$pkgName")
|
|
filearray=( ${(f)findfiles} )
|
|
if [[ ${#filearray} -eq 0 ]]; then
|
|
cleanupAndExit 20 "couldn't find pkg “$pkgName” in zip $archiveName" ERROR
|
|
fi
|
|
# it is now safe to overwrite archiveName for installFromPKG
|
|
archiveName="${filearray[1]}"
|
|
printlog "found pkg: $archiveName"
|
|
fi
|
|
fi
|
|
|
|
# installFromPkgs
|
|
installFromPKG
|
|
}
|
|
|
|
installAppInDmgInZip() {
|
|
# unzip the archive
|
|
printlog "Unzipping $archiveName"
|
|
tar -xf "$archiveName"
|
|
|
|
# locate dmg in zip
|
|
if [[ -z $pkgName ]]; then
|
|
# find first file ending with 'dmg'
|
|
findfiles=$(find "$tmpDir" -iname "*.dmg" -maxdepth 2 )
|
|
filearray=( ${(f)findfiles} )
|
|
if [[ ${#filearray} -eq 0 ]]; then
|
|
cleanupAndExit 20 "couldn't find dmg in zip $archiveName" ERROR
|
|
fi
|
|
archiveName="$(basename ${filearray[1]})"
|
|
# it is now safe to overwrite archiveName for installFromDMG
|
|
printlog "found dmg: $tmpDir/$archiveName"
|
|
else
|
|
# it is now safe to overwrite archiveName for installFromDMG
|
|
archiveName="$pkgName"
|
|
fi
|
|
|
|
# installFromDMG, DMG expected to include an app (will not work with pkg)
|
|
installFromDMG
|
|
}
|
|
|
|
runUpdateTool() {
|
|
printlog "Function called: runUpdateTool"
|
|
if [[ -x $updateTool ]]; then
|
|
printlog "running $updateTool $updateToolArguments"
|
|
if [[ -n $updateToolRunAsCurrentUser ]]; then
|
|
updateOutput=$(runAsUser $updateTool ${updateToolArguments} 2>&1)
|
|
updateStatus=$(echo $?)
|
|
else
|
|
updateOutput=$($updateTool ${updateToolArguments} 2>&1)
|
|
updateStatus=$(echo $?)
|
|
fi
|
|
sleep 1
|
|
updateEndTime=$(date "+$updateToolLogDateFormat")
|
|
deduplicatelogs $updateOutput
|
|
if [[ -n $updateToolLog ]]; then
|
|
updateOutput+=$(echo "Output of Installer log of $updateToolLog below this line.\n")
|
|
updateOutput+=$(echo "----------------------------------------------------------\n")
|
|
updateOutput+=$(awk -v "b=$updatestarttime" -v "e=$updateEndTime" -F ',' '$1 >= b && $1 <= e' $updateToolLog)
|
|
fi
|
|
|
|
if [[ $updateStatus -ne 0 ]]; then
|
|
printlog "Error running $updateTool, Procceding with normal installation. Exit Status: $updateStatus Error:\n$logoutput" WARN
|
|
return 1
|
|
if [[ $type == updateronly ]]; then
|
|
cleanupAndExit 77 "No Download URL Set, this is an update only application and the updater failed" WARN
|
|
fi
|
|
elif [[ $updateStatus -eq 0 ]]; then
|
|
printlog "Debugging enabled, update tool output was:\n$logoutput" DEBUG
|
|
fi
|
|
else
|
|
printlog "couldn't find $updateTool, continuing normally"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
finishing() {
|
|
printlog "Finishing..."
|
|
sleep 10 # wait a moment to let spotlight catch up
|
|
getAppVersion
|
|
|
|
if [[ -z $appversion ]]; then
|
|
message="Installed $name"
|
|
else
|
|
message="Installed $name, version $appversion"
|
|
fi
|
|
|
|
printlog "$message"
|
|
|
|
if [[ $currentUser != "loginwindow" && ( $NOTIFY == "success" || $NOTIFY == "all" ) ]]; then
|
|
printlog "notifying"
|
|
if [[ $updateDetected == "YES" ]]; then
|
|
displaynotification "$message" "$name update complete!"
|
|
else
|
|
displaynotification "$message" "$name installation complete!"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Detect if there is an app actively making a display sleep assertion, e.g.
|
|
# KeyNote, PowerPoint, Zoom, or Webex.
|
|
# See: https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes
|
|
hasDisplaySleepAssertion() {
|
|
# Get the names of all apps with active display sleep assertions
|
|
local apps="$(/usr/bin/pmset -g assertions | /usr/bin/awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ {gsub(/^.*\(/,"",$0); gsub(/\).*$/,"",$0); print};')"
|
|
|
|
if [[ ! "${apps}" ]]; then
|
|
# No display sleep assertions detected
|
|
return 1
|
|
fi
|
|
|
|
# Create an array of apps that need to be ignored
|
|
local ignore_array=("${(@s/,/)IGNORE_DND_APPS}")
|
|
|
|
for app in ${(f)apps}; do
|
|
if (( ! ${ignore_array[(Ie)${app}]} )); then
|
|
# Relevant app with display sleep assertion detected
|
|
printlog "Display sleep assertion detected by ${app}."
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# No relevant display sleep assertion detected
|
|
return 1
|
|
}
|
|
|