From fd19575cbfb7fe378169674ecba28de4c8d50e3c Mon Sep 17 00:00:00 2001 From: Michael <30936296+mtan93@users.noreply.github.com> Date: Mon, 12 Sep 2022 11:51:58 +0100 Subject: [PATCH] Update hpclick.sh --- hpclick.sh | 1847 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1806 insertions(+), 41 deletions(-) diff --git a/hpclick.sh b/hpclick.sh index e1cc650..1c0dc4a 100644 --- a/hpclick.sh +++ b/hpclick.sh @@ -21,57 +21,1822 @@ #################################################################################################### # Script to download and install AccountEdge Network Edition. # +#!/bin/zsh +label="hpclicknp" # if no label is sent to the script, this will be used -downloadUrl='https://ftp.hp.com/pub/softlib/software13/hpdesignjetclick/HPClick-3.5.263.dmg' -dnldfile='HPClick-3.5.263.dmg' -appName='HP Click' -forceQuit='N' -logfile="/Library/Caches/com.purplecomputing.mdm/Logs/$appName-InstallScript.log" -deplog="/var/tmp/depnotify.log" -OSvers_URL=$( sw_vers -productVersion | sed 's/[.]/_/g' ) -userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X ${OSvers_URL}) AppleWebKit/535.6.2 (KHTML, like Gecko) Version/5.2 Safari/535.6.2" -scriptver="1.0" +# Installomator +# +# Downloads and installs Applications +# 2020-2021 Installomator +# +# inspired by the download scripts from William Smith and Sander Schram +# +# Contributers: +# Armin Briegel - @scriptingosx +# Isaac Ordonez - @issacatmann +# Søren Theilgaard - @Theile +# Adam Codega - @acodega +# +# with contributions from many others + +export PATH=/usr/bin:/bin:/usr/sbin:/sbin + +# NOTE: adjust these variables: + +# set to 0 for production, 1 or 2 for debugging +# while debugging, items will be downloaded to the parent directory of this script +# also no actual installation will be performed +# debug mode 1 will download to the directory the script is run in, but will not check the version +# debug mode 2 will download to the temp directory, check for blocking processes, check the version, but will not install anything or remove the current version +DEBUG=0 + +# notify behavior +NOTIFY=success +# options: +# - success notify the user on success +# - silent no notifications +# - all all notifications (great for Self Service installation) + +# behavior when blocking processes are found +BLOCKING_PROCESS_ACTION=tell_user +# options: +# - ignore continue even when blocking processes are found +# - quit app will be told to quit nicely if running +# - quit_kill told to quit twice, then it will be killed +# Could be great for service apps if they do not respawn +# - silent_fail exit script without prompt or installation +# - prompt_user show a user dialog for each blocking process found, +# user can choose "Quit and Update" or "Not Now". +# When "Quit and Update" is chosen, blocking process +# will be told to quit. Installomator will wait 30 seconds +# before checking again in case Save dialogs etc are being responded to. +# Installomator will abort if quitting after three tries does not succeed. +# "Not Now" will exit Installomator. +# - prompt_user_then_kill +# show a user dialog for each blocking process found, +# user can choose "Quit and Update" or "Not Now". +# When "Quit and Update" is chosen, blocking process +# will be terminated. Installomator will abort if terminating +# after two tries does not succeed. "Not Now" will exit Installomator. +# - prompt_user_loop +# Like prompt-user, but clicking "Not Now", will just wait an hour, +# and then it will ask again. +# WARNING! It might block the MDM agent on the machine, as +# the script will not exit, it will pause until the hour has passed, +# possibly blocking for other management actions in this time. +# - tell_user User will be showed a notification about the important update, +# but user is only allowed to Quit and Continue, and then we +# ask the app to quit. This is default. +# - tell_user_then_kill +# User will be showed a notification about the important update, +# but user is only allowed to Quit and Continue. If the quitting fails, +# the blocking processes will be terminated. +# - kill kill process without prompting or giving the user a chance to save -# Making Purple Cache directories for in the event that the helper script hasn't been run -mkdir -p /Library/Caches/com.purplecomputing.mdm/ -mkdir -p /Library/Caches/com.purplecomputing.mdm/Logs/ -mkdir -p /Library/Caches/com.purplecomputing.mdm/Apps/ +# logo-icon used in dialog boxes if app is blocking +LOGO=appstore +# options: +# - appstore Icon is Apple App Store (default) +# - jamf JAMF Pro +# - mosyleb Mosyle Business +# - mosylem Mosyle Manager (Education) +# - addigy Addigy +# - microsoft Microsoft Endpoint Manager (Intune) +# - ws1 Workspace ONE (AirWatch) +# path can also be set in the command call, and if file exists, it will be used. +# Like 'LOGO="/System/Applications/App\ Store.app/Contents/Resources/AppIcon.icns"' +# (spaces have to be escaped). -/bin/echo "Status: Installing ${appName}" >> ${deplog} -/bin/echo "Status: Installing ${appName}" >> ${logfile} +# App Store apps handling +IGNORE_APP_STORE_APPS=no +# options: +# - no If the installed app is from App Store (which include VPP installed apps) +# it will not be touched, no matter its version (default) +# - yes Replace App Store (and VPP) version of the app and handle future +# updates using Installomator, even if latest version. +# Shouldn’t give any problems for the user in most cases. +# Known bad example: Slack will lose all settings. -if [[ $@ == "openconsole" ]]; then - open ${logfile} - open ${deplog} +# Owner of copied apps +SYSTEMOWNER=0 +# options: +# - 0 Current user will be owner of copied apps, just like if they +# installed it themselves (default). +# - 1 root:wheel will be set on the copied app. +# Useful for shared machines. + +# install behavior +INSTALL="" +# options: +# - When not set, the software will only be installed +# if it is newer/different in version +# - force Install even if it’s the same version + + +# Re-opening of closed app +REOPEN="yes" +# options: +# - yes App will be reopened if it was closed +# - no App not reopened + +# Only let Installomator return the name of the label +# RETURN_LABEL_NAME=0 +# options: +# - 1 Installomator will return the name of the label and exit, so last line of +# output will be that name. When Installomator is locally installed and we +# use DEPNotify, then DEPNotify can present a more nice name to the user, +# instead of just the label name. + + +# Interrupt Do Not Disturb (DND) full screen apps +INTERRUPT_DND="yes" +# options: +# - yes Script will run without checking for DND full screen apps. +# - no Script will exit when an active DND full screen app is detected. + +# Comma separated list of app names to ignore when evaluating DND +IGNORE_DND_APPS="" +# example that will ignore browsers when evaluating DND: +# IGNORE_DND_APPS="firefox,Google Chrome,Safari,Microsoft Edge,Opera,Amphetamine,caffeinate" + + +# Swift Dialog integration + +# These variables will allow Installomator to communicate progress with Swift Dialog +# https://github.com/bartreardon/swiftDialog + +# This requires Swift Dialog 2.11.2 or higher. + +DIALOG_CMD_FILE="" +# When this variable is set, Installomator will write Swift Dialog commands to this path. +# Installomator will not launch Swift Dialog. The process calling Installomator will have +# launch and configure Swift Dialog to listen to this file. +# See `MDM/swiftdialog_example.sh` for an example. + +DIALOG_LIST_ITEM_NAME="" +# When this variable is set, progress for downloads and installs will be sent to this +# listitem. +# When the variable is unset, progress will be sent to Swift Dialog's main progress bar. + + + +# NOTE: How labels work + +# Each workflow label needs to be listed in the case statement below. +# for each label these variables can be set: +# +# - name: (required) +# Name of the installed app. +# This is used to derive many of the other variables. +# +# - type: (required) +# The type of the installation. Possible values: +# - dmg +# - pkg +# - zip +# - tbz +# - pkgInDmg +# - pkgInZip +# - appInDmgInZip +# - updateronly This last one is for labels that should only run an updateTool (see below) +# +# - packageID: (optional) +# The package ID of a pkg +# If given, will be used to find the version of installed software, instead of searching for an app. +# Usefull if a pkg does not install an app. +# See label installomator_st +# +# - downloadURL: (required) +# URL to download the dmg. +# Can be generated with a series of commands (see BBEdit for an example). +# +# - curlOptions: (array, optional) +# Options to the curl command, needed for curl to be able to download the software. +# Usually used for adding extra headers that some servers need in order to serve the file. +# curlOptions=( -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15" ) +# (See “mocha”-labels, for examples on labels, and buildLabel.sh for header-examples.) +# +# - appNewVersion: (optional) +# Version of the downloaded software. +# If given, it will be compared to the installed version, to see if the download is different. +# It does not check for newer or not, only different. +# +# - versionKey: (optional) +# How we get version number from app. Possible values: +# - CFBundleShortVersionString +# - CFBundleVersion +# Not all software titles uses fields the same. +# See Opera label. +# +# - appCustomVersion(){}: (optional function) +# This function can be added to your label, if a specific custom +# mechanism hs to be used for getting the installed version. +# See labels zulujdk11, zulujdk13, zulujdk15 +# +# - expectedTeamID: (required) +# 10-digit developer team ID. +# Obtain the team ID by running: +# +# - Applications (in dmgs or zips) +# spctl -a -vv /Applications/BBEdit.app +# +# - Pkgs +# spctl -a -vv -t install ~/Downloads/desktoppr-0.2.pkg +# +# The team ID is the ten-digit ID at the end of the line starting with 'origin=' +# +# - archiveName: (optional) +# The name of the downloaded file. +# When not given the archiveName is derived from the $name. +# Note: This has to be defined BEFORE calling downloadURLFromGit or +# versionFromGit functions in the label. +# +# - appName: (optional) +# File name of the app bundle in the dmg to verify and copy (include .app). +# When not given, the appName is derived from the $name. +# +# - targetDir: (optional) +# dmg or zip: +# Applications will be copied to this directory. +# Default value is '/Applications' for dmg and zip installations. +# pkg: +# targetDir is used as the install-location. Default is '/'. +# +# - blockingProcesses: (optional) +# Array of process names that will block the installation or update. +# If no blockingProcesses array is given the default will be: +# blockingProcesses=( $name ) +# When a package contains multiple applications, _all_ should be listed, e.g: +# blockingProcesses=( "Keynote" "Pages" "Numbers" ) +# When a workflow has no blocking processes, use +# blockingProcesses=( NONE ) +# +# - pkgName: (optional, only used for pkgInDmg, dmgInZip, and appInDmgInZip) +# File name or path to the pkg/dmg file _inside_ the dmg or zip. +# When not given the pkgName is derived from the $name +# +# - updateTool: +# - updateToolArguments: +# When Installomator detects an existing installation of the application, +# and the updateTool variable is set +# $updateTool $updateArguments +# Will be run instead of of downloading and installing a complete new version. +# Use this when the updateTool does differential and optimized downloads. +# e.g. msupdate on various Microsoft labels +# +# - updateToolRunAsCurrentUser: +# When this variable is set (any value), $updateTool will be run as the current user. +# +# - CLIInstaller: +# - CLIArguments: +# If the downloaded dmg is an installer that we can call using CLI, we can +# use these two variables for what to call. +# We need to define `name` for the installed app (to be version checked), as well as +# `installerTool` for the installer app (if named differently than `name`. Installomator +# will add the path to the folder/disk image with the binary, and it will be called like this: +# $CLIInstaller $CLIArguments +# For most installations `CLIInstaller` should contain the `installerTool` for the CLI call +# (if it’s the same). +# We can support a whole range of other software titles by implementing this. +# See label adobecreativeclouddesktop +# +# - installerTool: +# Introduced as part of `CLIInstaller`. If the installer in the DMG or ZIP is named +# differently than the installed app, then this variable can be used to name the +# installer that should be located after mounting/expanding the downloaded archive. +# See label adobecreativeclouddesktop +# +### Logging +# Logging behavior +LOGGING="INFO" +# options: +# - DEBUG Everything is logged +# - INFO (default) normal logging level +# - WARN only warning +# - ERROR only errors +# - REQ ???? + +# MDM profile name +MDMProfileName="" +# options: +# - MDM Profile Addigy has this name on the profile +# - Mosyle Corporation MDM Mosyle uses this name on the profile +# From the LOGO variable we can know if Addigy og Mosyle is used, so if that variable +# is either of these, and this variable is empty, then we will auto detect this. + +# Datadog logging used +datadogAPI="" +# Simply add your own API key for this in order to have logs sent to Datadog +# See more here: https://www.datadoghq.com/product/log-management/ + +# Log Date format used when parsing logs for debugging, this is the default used by +# install.log, override this in the case statements if you need something custom per +# application (See adobeillustrator). Using stadard GNU Date formatting. +LogDateFormat="%Y-%m-%d %H:%M:%S" + +# Get the start time for parsing install.log if we fail. +starttime=$(date "+$LogDateFormat") + +# Check if we have rosetta installed +if [[ $(/usr/bin/arch) == "arm64" ]]; then + if ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then # pgrep oahd >/dev/null 2>&1 + rosetta2=no + fi +fi +VERSION="10.0beta2" +VERSIONDATE="2022-09-02" + +# 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 + updateDialog "fail" "Error ($1; $2)" + else + printlog "$2" $3 + updateDialog "success" "" + fi + printlog "################## End Installomator, exit code $1 \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" + hubcli="/usr/local/bin/hubcli" + + if [[ -x "$manageaction" ]]; then + "$manageaction" -message "$message" -title "$title" + elif [[ -x "$hubcli" ]]; then + "$hubcli" notify -t "$title" -i "$message" -c "Dismiss" + else + runAsUser osascript -e "display notification \"$message\" with title \"$title\"" + fi +} + +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 greater than 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 + + # Extra spaces + space_char="" + if [[ ${#log_priority} -eq 3 ]]; then + space_char=" " + elif [[ ${#log_priority} -eq 4 ]]; then + space_char=" " + 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}${space_char} : $label : ${logmessage}" | tee -a $log_location + else + echo "$timestamp" : "${log_priority}${space_char} : $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 -L --silent --fail "https://api.github.com/repos/$gitusername/$gitreponame/releases/latest" | awk -F '"' "/browser_download_url/ && /$archiveName\"/ { print \$4; exit }") + downloadURL=https://github.com$(curl -sfL "https://github.com/$gitusername/$gitreponame/releases/latest" | tr '"' "\n" | grep -i "^/.*\/releases\/download\/.*$archiveName" | head -1) + else + #downloadURL=$(curl -L --silent --fail "https://api.github.com/repos/$gitusername/$gitreponame/releases/latest" | awk -F '"' "/browser_download_url/ && /$filetype\"/ { print \$4; exit }") + downloadURL=https://github.com$(curl -sfL "https://github.com/$gitusername/$gitreponame/releases/latest" | tr '"' "\n" | grep -i "^/.*\/releases\/download\/.*\.$filetype" | head -1) + fi + if [ -z "$downloadURL" ]; then + cleanupAndExit 14 "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 -L --silent --fail "https://api.github.com/repos/$gitusername/$gitreponame/releases/latest" | grep tag_name | cut -d '"' -f 4 | sed 's/[^0-9\.]//g') + appNewVersion=$(curl -sLI "https://github.com/$gitusername/$gitreponame/releases/latest" | grep -i "^location" | tr "/" "\n" | tail -1 | sed 's/[^0-9\.]//g') + if [ -z "$appNewVersion" ]; then + printlog "could not retrieve version number for $gitusername/$gitreponame" WARN + 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 +} + +# from @Pico: https://macadmins.slack.com/archives/CGXNNJXJ9/p1652222365989229?thread_ts=1651786411.413349&cid=CGXNNJXJ9 +getJSONValue() { + # $1: JSON string OR file path to parse (tested to work with up to 1GB string and 2GB file). + # $2: JSON key path to look up (using dot or bracket notation). + printf '%s' "$1" | /usr/bin/osascript -l 'JavaScript' \ + -e "let json = $.NSString.alloc.initWithDataEncoding($.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile$(/usr/bin/uname -r | /usr/bin/awk -F '.' '($1 > 18) { print "AndReturnError(ObjC.wrap())" }'), $.NSUTF8StringEncoding)" \ + -e 'if ($.NSFileManager.defaultManager.fileExistsAtPath(json)) json = $.NSString.stringWithContentsOfFileEncodingError(json, $.NSUTF8StringEncoding, ObjC.wrap())' \ + -e "const value = JSON.parse(json.js)$([ -n "${2%%[.[]*}" ] && echo '.')$2" \ + -e 'if (typeof value === "object") { JSON.stringify(value, null, 4) } else { value }' +} + +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 targetDir, /Applications, or /Applications/Utilities + if [[ -d "$targetDir/$appName" ]]; then + applist="$targetDir/$appName" + elif [[ -d "/Applications/$appName" ]]; then + applist="/Applications/$appName" +# if [[ $type =~ '^(dmg|zip|tbz|app.*)$' ]]; then +# targetDir="/Applications" +# fi + elif [[ -d "/Applications/Utilities/$appName" ]]; then + applist="/Applications/Utilities/$appName" +# if [[ $type =~ '^(dmg|zip|tbz|app.*)$' ]]; then +# targetDir="/Applications/Utilities" +# fi + else + # applist=$(mdfind "kind:application $appName" -0 ) + printlog "name: $name, appName: $appName" + applist=$(mdfind "kind:application AND name:$name" -0 ) +# printlog "App(s) found: ${applist}" DEBUG +# applist=$(mdfind "kind:application AND name:$appName" -0 ) + fi + if [[ -z $applist ]]; then + printlog "No previous app found" WARN + else + printlog "App(s) found: ${applist}" INFO + fi +# if [[ $type =~ '^(dmg|zip|tbz|app.*)$' ]]; then +# printlog "targetDir for installation: $targetDir" INFO +# 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" WARN + appversion=0 + else + cleanupAndExit 23 "App previously installed from App Store, and we respect that" ERROR + fi + fi + else + printlog "could not determine location of $appName" WARN + fi + else + printlog "could not find $appName" WARN + 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." INFO + 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" ERROR + fi + + # verify with spctl + printlog "Verifying: $appPath" INFO + updateDialog "wait" "Verifying..." + printlog "App size: $(du -sh "$appPath")" DEBUG + 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" REG + else + printlog "Using force to install anyway." + fi + elif [[ -z $appversion ]]; then + printlog "Installing $name version $appNewVersion on versionKey $versionKey." + 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 15 "Installed macOS is too old for this app." ERROR + 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" WARN + deleteAppOut=$(rm -Rfv "$targetDir/$appName" 2>&1) + tempName="$targetDir/$appName" + tempNameLength=$((${#tempName} + 10)) + deleteAppOut=$(echo $deleteAppOut | cut -c 1-$tempNameLength) + deduplicatelogs "$deleteAppOut" + printlog "Debugging enabled, App removing output was:\n$logoutput" DEBUG + fi + + # copy app to /Applications + printlog "Copy $appPath to $targetDir" + copyAppOut=$(ditto -v "$appPath" "$targetDir/$appName" 2>&1) + copyAppStatus=$(echo $?) + deduplicatelogs "$copyAppOut" + printlog "Debugging enabled, App copy output was:\n$logoutput" DEBUG + if [[ $copyAppStatus -ne 0 ]] ; then + #if ! ditto "$appPath" "$targetDir/$appName"; then + cleanupAndExit 7 "Error while copying:\n$logoutput" ERROR + fi + + # set ownership to current user + if [[ "$currentUser" != "loginwindow" && $SYSTEMOWNER -ne 1 ]]; then + printlog "Changing owner to $currentUser" WARN + chown -R "$currentUser" "$targetDir/$appName" + else + printlog "No user logged in or SYSTEMOWNER=1, setting owner to root:wheel" WARN + 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 $?) + deduplicatelogs "$CLIoutput" + + if [ $CLIstatus -ne 0 ] ; then + cleanupAndExit 16 "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- ) + 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, dmgmount output was:\n$logoutput" DEBUG + + printlog "Mounted: $dmgmount" INFO +} + +installFromDMG() { + mountDMG + installAppWithPath "$dmgmount/$appName" +} + +installFromPKG() { + # verify with spctl + printlog "Verifying: $archiveName" + updateDialog "wait" "Verifying..." + printlog "File list: $(ls -lh "$archiveName")" DEBUG + printlog "File type: $(file "$archiveName")" DEBUG + 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 '()' ) + # Apple signed software has no teamID, grab entire origin instead + if [[ -z $teamID ]]; then + teamID=$(echo $spctlOut | awk -F '=' '/origin=/ {print $NF }') + fi + + 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" REQ + 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" + + if [[ $DIALOG_CMD_FILE != "" ]]; then + # pipe + pipe="$tmpDir/installpipe" + # initialise named pipe for installer output + initNamedPipe create $pipe + + # run the pipe read in the background + readPKGInstallPipe $pipe "$DIALOG_CMD_FILE" & installPipePID=$! + printlog "listening to output of installer with pipe $pipe and command file $DIALOG_CMD_FILE on PID $installPipePID" DEBUG + + pkgInstall=$(installer -verboseR -pkg "$archiveName" -tgt "$targetDir" 2>&1 | tee $pipe) + pkgInstallStatus=$pipestatus[1] + # because we are tee-ing the output, we want the pipe status of the first command in the chain, not the most recent one + killProcess $installPipePID + + else + pkgInstall=$(installer -verbose -dumplog -pkg "$archiveName" -tgt "$targetDir" 2>&1) + pkgInstallStatus=$(echo $?) + fi + + + + sleep 1 + pkgEndTime=$(date "+$LogDateFormat") + pkgInstall+=$(echo "\nOutput 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" -type f -maxdepth 1 ) + printlog "Found pkg(s):\n$findfiles" DEBUG + filearray=( ${(f)findfiles} ) + if [[ ${#filearray} -eq 0 ]]; then + cleanupAndExit 20 "couldn't find pkg in dmg $archiveName" ERROR + fi + archiveName="${filearray[1]}" + else + if [[ -s "$dmgmount/$pkgName" ]] ; then # was: $tmpDir + archiveName="$dmgmount/$pkgName" + else + # try searching for pkg + findfiles=$(find "$dmgmount" -iname "$pkgName") # was: $tmpDir + printlog "Found pkg(s):\n$findfiles" DEBUG + filearray=( ${(f)findfiles} ) + if [[ ${#filearray} -eq 0 ]]; then + cleanupAndExit 20 "couldn't find pkg “$pkgName” in dmg $archiveName" ERROR + fi + # it is now safe to overwrite archiveName for installFromPKG + archiveName="${filearray[1]}" + fi + fi + printlog "found pkg: $archiveName" + + # 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" -type f -maxdepth 2 ) + printlog "Found pkg(s):\n$findfiles" DEBUG + filearray=( ${(f)findfiles} ) + if [[ ${#filearray} -eq 0 ]]; then + cleanupAndExit 21 "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 [[ -s "$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 21 "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 22 "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" ERROR + 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" WARN + return 1 + fi + return 0 +} + +finishing() { + printlog "Finishing..." + + sleep 3 # 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" REQ + + 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 +} + +initNamedPipe() { + # create or delete a named pipe + # commands are "create" or "delete" + + local cmd=$1 + local pipe=$2 + case $cmd in + "create") + if [[ -e $pipe ]]; then + rm $pipe + fi + # make named pipe + mkfifo -m 644 $pipe + ;; + "delete") + # clean up + rm $pipe + ;; + *) + ;; + esac +} + +readDownloadPipe() { + # reads from a previously created named pipe + # output from curl with --progress-bar. % downloaded is read in and then sent to the specified log file + local pipe=$1 + local log=${2:-$DIALOG_CMD_FILE} + # set up read from pipe + while IFS= read -k 1 -u 0 char; do + if [[ $char =~ [0-9] ]]; then + keep=1 + fi + + if [[ $char == % ]]; then + updateDialog $progress "Downloading..." + progress="" + keep=0 + fi + + if [[ $keep == 1 ]]; then + progress="$progress$char" + fi + done < $pipe +} + +readPKGInstallPipe() { + # reads from a previously created named pipe + # output from installer with -verboseR. % install status is read in and then sent to the specified log file + local pipe=$1 + local log=${2:-$DIALOG_CMD_FILE} + local appname=${3:-$name} + + while read -k 1 -u 0 char; do + if [[ $char == % ]]; then + keep=1 + fi + if [[ $char =~ [0-9] && $keep == 1 ]]; then + progress="$progress$char" + fi + if [[ $char == . && $keep == 1 ]]; then + updateDialog $progress "Installing..." + progress="" + keep=0 + fi + done < $pipe +} + +killProcess() { + # will silently kill the specified PID + builtin kill $1 2>/dev/null +} + +updateDialog() { + local state=$1 + local message=$2 + local listitem=${3:-$DIALOG_LIST_ITEM_NAME} + local cmd_file=${4:-$DIALOG_CMD_FILE} + local progress="" + + if [[ $state =~ '^[0-9]' \ + || $state == "reset" \ + || $state == "increment" \ + || $state == "complete" \ + || $state == "indeterminate" ]]; then + progress=$state + fi + + # when to cmdfile is set, do nothing + if [[ $cmd_file == "" ]]; then + return + fi + + if [[ $listitem == "" ]]; then + # no listitem set, update main progress bar and progress text + if [[ $progress != "" ]]; then + echo "progress: $progress" >> $cmd_file + fi + if [[ $message != "" ]]; then + echo "progresstext: $name - $message" >> $cmd_file + fi + else + # list item has a value, so we update the progress and text in the list + if [[ $progress != "" ]]; then + echo "listitem: title: $listitem, statustext: $message, progress: $progress" >> $cmd_file + else + echo "listitem: title: $listitem, statustext: $message, status: $state" >> $cmd_file + fi + fi +} +# MARK: check minimal macOS requirement +autoload is-at-least + +installedOSversion=$(sw_vers -productVersion) +if ! is-at-least 10.14 $installedOSversion; then + printlog "Installomator requires at least macOS 10.14 Mojave." ERROR + exit 98 fi -/bin/echo "Script Version: ${scriptver}" >> ${logfile} - /bin/echo "`date`: Downloading." >> ${logfile} - /bin/echo "Downloading." - /usr/bin/curl -L -o /Library/Caches/com.purplecomputing.mdm/Apps/${dnldfile} ${downloadUrl} - if [[ "${forceQuit}" = "Y" ]]; then - killall ${appName} - fi +# MARK: argument parsing +if [[ $# -eq 0 ]]; then + if [[ -z $label ]]; then # check if label is set inside script + printlog "no label provided, printing labels" REQ + grep -E '^[a-z0-9\_-]*(\)|\|\\)$' "$0" | tr -d ')|\' | grep -v -E '^(broken.*|longversion|version|valuesfromarguments)$' | sort + #grep -E '^[a-z0-9\_-]*(\)|\|\\)$' "${labelFile}" | tr -d ')|\' | grep -v -E '^(broken.*|longversion|version|valuesfromarguments)$' | sort + exit 0 + fi +elif [[ $1 == "/" ]]; then + # jamf uses sends '/' as the first argument + printlog "shifting arguments for Jamf" REQ + shift 3 +fi - cd /Library/Caches/com.purplecomputing.mdm/Apps/ -# unzip ${dnldfile} +while [[ -n $1 ]]; do + if [[ $1 =~ ".*\=.*" ]]; then + # if an argument contains an = character, send it to eval + printlog "setting variable from argument $1" INFO + eval $1 + else + # assume it's a label + label=$1 + fi + # shift to next argument + shift 1 +done - /bin/echo "`date`: Mounting installer disk image." >> ${logfile} - /usr/bin/hdiutil attach /Library/Caches/com.purplecomputing.mdm/Apps/${dnldfile} -nobrowse -quiet - /bin/echo "`date`: Installing..." >> ${logfile} - ditto -rsrc "/Volumes/"${appName}/${appName}".app" "/Applications/"${appName}".app - - /bin/sleep 10 - /bin/echo "`date`: Unmounting installer disk image." >> ${logfile} - /usr/bin/hdiutil detach $(/bin/df | /usr/bin/grep AppName | awk '{print $1}') -quiet - /bin/sleep 10 - +# lowercase the label +label=${label:l} - /bin/sleep 5 - /bin/echo "`date`: Deleting the downloaded file." >> ${logfile} - /bin/rm /Library/Caches/com.purplecomputing.mdm/Apps/${dnldfile} +# separate check for 'version' in order to print plain version number without any other information +if [[ $label == "version" ]]; then + echo "$VERSION" + exit 0 +fi - /bin/echo "Command: DeterminateManualStep: 1" >> ${deplog} +# 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 + +# Mark: START +printlog "################## Start Installomator v. $VERSION, date $VERSIONDATE" REQ +printlog "################## Version: $VERSION" INFO +printlog "################## Date: $VERSIONDATE" INFO +printlog "################## $label" INFO + +# Check for DEBUG mode +if [[ $DEBUG -gt 0 ]]; then + printlog "DEBUG mode $DEBUG enabled." DEBUG +fi + +# How we get version number from app +if [[ -z $versionKey ]]; then + versionKey="CFBundleShortVersionString" +fi + +# get current user +currentUser=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') + +# MARK: check for root +if [[ "$(whoami)" != "root" && "$DEBUG" -eq 0 ]]; then + # not running as root + cleanupAndExit 6 "not running as root, exiting" ERROR +fi + + +# check Swift Dialog presence and version +DIALOG_CMD="/usr/local/bin/dialog" + +if [[ ! -x $DIALOG_CMD ]]; then + # Swift Dialog is not installed, clear cmd file variable to ignore + printlog "SwiftDialog is not installed, clear cmd file var" + DIALOG_CMD_FILE="" +fi + +# MARK: labels in case statement +case $label in +longversion) + # print the script version + printlog "Installomater: version $VERSION ($VERSIONDATE)" REQ + exit 0 + ;; +valuesfromarguments) + if [[ -z $name ]]; then + printlog "need to provide 'name'" ERROR + exit 1 + fi + if [[ -z $type ]]; then + printlog "need to provide 'type'" ERROR + exit 1 + fi + if [[ -z $downloadURL ]]; then + printlog "need to provide 'downloadURL'" ERROR + exit 1 + fi + if [[ -z $expectedTeamID ]]; then + printlog "need to provide 'expectedTeamID'" ERROR + exit 1 + fi + ;; + +# label descriptions start here +hpclicknp) + name="HP Click" + type="dmg" + downloadURL="https://ftp.hp.com/pub/softlib/software13/hpdesignjetclick/HPClick-3.5.263.dmg" + expectedTeamID="6HB5Y2QTA3" + ;; +*) + # unknown label + #printlog "unknown label $label" + cleanupAndExit 1 "unknown label $label" ERROR + ;; +esac + +# Are we only asked to return label name +if [[ $RETURN_LABEL_NAME -eq 1 ]]; then + printlog "Only returning label name." REQ + printlog "$name" + echo "$name" + exit +fi + +# MARK: application download and installation starts here + +# Debug output of all variables in a label +printlog "name=${name}" DEBUG +printlog "appName=${appName}" DEBUG +printlog "type=${type}" DEBUG +printlog "archiveName=${archiveName}" DEBUG +printlog "downloadURL=${downloadURL}" DEBUG +printlog "curlOptions=${curlOptions}" DEBUG +printlog "appNewVersion=${appNewVersion}" DEBUG +printlog "appCustomVersion function: $(if type 'appCustomVersion' 2>/dev/null | grep -q 'function'; then echo "Defined. ${appCustomVersion}"; else; echo "Not defined"; fi)" DEBUG +printlog "versionKey=${versionKey}" DEBUG +printlog "packageID=${packageID}" DEBUG +printlog "pkgName=${pkgName}" DEBUG +printlog "choiceChangesXML=${choiceChangesXML}" DEBUG +printlog "expectedTeamID=${expectedTeamID}" DEBUG +printlog "blockingProcesses=${blockingProcesses}" DEBUG +printlog "installerTool=${installerTool}" DEBUG +printlog "CLIInstaller=${CLIInstaller}" DEBUG +printlog "CLIArguments=${CLIArguments}" DEBUG +printlog "updateTool=${updateTool}" DEBUG +printlog "updateToolArguments=${updateToolArguments}" DEBUG +printlog "updateToolRunAsCurrentUser=${updateToolRunAsCurrentUser}" DEBUG +#printlog "Company=${Company}" DEBUG # Not used + +if [[ ${INTERRUPT_DND} = "no" ]]; then + # Check if a fullscreen app is active + if hasDisplaySleepAssertion; then + cleanupAndExit 24 "active display sleep assertion detected, aborting" ERROR + fi +fi + +printlog "BLOCKING_PROCESS_ACTION=${BLOCKING_PROCESS_ACTION}" +printlog "NOTIFY=${NOTIFY}" +printlog "LOGGING=${LOGGING}" + +# Finding LOGO to use in dialogs +case $LOGO in + appstore) + # Apple App Store on Mac + if [[ $(sw_vers -buildVersion) > "19" ]]; then + LOGO="/System/Applications/App Store.app/Contents/Resources/AppIcon.icns" + else + LOGO="/Applications/App Store.app/Contents/Resources/AppIcon.icns" + fi + ;; + jamf) + # Jamf Pro + LOGO="/Library/Application Support/JAMF/Jamf.app/Contents/Resources/AppIcon.icns" + ;; + mosyleb) + # Mosyle Business + LOGO="/Applications/Self-Service.app/Contents/Resources/AppIcon.icns" + if [[ -z $MDMProfileName ]]; then; MDMProfileName="Mosyle Corporation MDM"; fi + ;; + mosylem) + # Mosyle Manager (education) + LOGO="/Applications/Manager.app/Contents/Resources/AppIcon.icns" + if [[ -z $MDMProfileName ]]; then; MDMProfileName="Mosyle Corporation MDM"; fi + ;; + addigy) + # Addigy + LOGO="/Library/Addigy/macmanage/MacManage.app/Contents/Resources/atom.icns" + if [[ -z $MDMProfileName ]]; then; MDMProfileName="MDM Profile"; fi + ;; + microsoft) + # Microsoft Endpoint Manager (Intune) + LOGO="/Library/Intune/Microsoft Intune Agent.app/Contents/Resources/AppIcon.icns" + if [[ -z $MDMProfileName ]]; then; MDMProfileName="Management Profile"; fi + ;; + ws1) + # Workspace ONE (AirWatch) + LOGO="/Applications/Workspace ONE Intelligent Hub.app/Contents/Resources/AppIcon.icns" + if [[ -z $MDMProfileName ]]; then; MDMProfileName="Device Manager"; fi + ;; +esac +if [[ ! -a "${LOGO}" ]]; then + if [[ $(sw_vers -buildVersion) > "19" ]]; then + LOGO="/System/Applications/App Store.app/Contents/Resources/AppIcon.icns" + else + LOGO="/Applications/App Store.app/Contents/Resources/AppIcon.icns" + fi +fi +printlog "LOGO=${LOGO}" INFO + +printlog "Label type: $type" INFO + +# MARK: extract info from data +if [ -z "$archiveName" ]; then + case $type in + dmg|pkg|zip|tbz|bz2) + archiveName="${name}.$type" + ;; + pkgInDmg) + archiveName="${name}.dmg" + ;; + *InZip) + archiveName="${name}.zip" + ;; + updateronly) + ;; + *) + printlog "Cannot handle type $type" + cleanupAndExit 99 + ;; + esac +fi +printlog "archiveName: $archiveName" INFO + +if [ -z "$appName" ]; then + # when not given derive from name + appName="$name.app" +fi + +if [ -z "$targetDir" ]; then + case $type in + dmg|zip|tbz|bz2|app*) + targetDir="/Applications" + ;; + pkg*) + targetDir="/" + ;; + updateronly) + ;; + *) + cleanupAndExit 99 "Cannot handle type $type" ERROR + ;; + esac +fi + +if [[ -z $blockingProcesses ]]; then + printlog "no blocking processes defined, using $name as default" INFO + blockingProcesses=( $name ) +fi + +# MARK: determine tmp dir +if [ "$DEBUG" -eq 1 ]; then + # for debugging use script dir as working directory + tmpDir=$(dirname "$0") +else + # create temporary working directory + tmpDir=$(mktemp -d ) +fi + +# MARK: change directory to temporary working directory +printlog "Changing directory to $tmpDir" DEBUG +if ! cd "$tmpDir"; then + cleanupAndExit 13 "error changing directory $tmpDir" ERROR +fi + +# MARK: get installed version +getAppVersion +printlog "appversion: $appversion" + +# MARK: Exit if new version is the same as installed version (appNewVersion specified) +if [[ "$type" != "updateronly" && ($INSTALL == "force" || $IGNORE_APP_STORE_APPS == "yes") ]]; then + printlog "Label is not of type “updateronly”, and it’s set to use force to install or ignoring app store apps, so not using updateTool." + updateTool="" +fi +if [[ -n $appNewVersion ]]; then + printlog "Latest version of $name is $appNewVersion" + if [[ $appversion == $appNewVersion ]]; then + if [[ $DEBUG -ne 1 ]]; then + printlog "There is no newer version available." + 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 newer version." REQ + fi + else + printlog "DEBUG mode 1 enabled, not exiting, but there is no new version of app." WARN + fi + fi +else + printlog "Latest version not specified." +fi + +# MARK: check if this is an Update and we can use updateTool +if [[ (-n $appversion && -n "$updateTool") || "$type" == "updateronly" ]]; then + printlog "appversion & updateTool" + updateDialog "wait" "Updating..." + + if [[ $DEBUG -ne 1 ]]; then + if runUpdateTool; then + finishing + cleanupAndExit 0 "updateTool has run" REQ + elif [[ $type == "updateronly" ]];then + cleanupAndExit 0 "type is $type so we end here." REQ + fi # otherwise continue + else + printlog "DEBUG mode 1 enabled, not running update tool" WARN + fi +fi + +# MARK: download the archive +if [ -f "$archiveName" ] && [ "$DEBUG" -eq 1 ]; then + printlog "$archiveName exists and DEBUG mode 1 enabled, skipping download" +else + # download + printlog "Downloading $downloadURL to $archiveName" REQ + if [[ $currentUser != "loginwindow" && $NOTIFY == "all" ]]; then + printlog "notifying" + if [[ $updateDetected == "YES" ]]; then + displaynotification "Downloading $name update" "Download in progress …" + else + displaynotification "Downloading new $name" "Download in progress …" + fi + fi + + if [[ $DIALOG_CMD_FILE != "" ]]; then + # pipe + pipe="$tmpDir/downloadpipe" + # initialise named pipe for curl output + initNamedPipe create $pipe + + # run the pipe read in the background + readDownloadPipe $pipe "$DIALOG_CMD_FILE" & downloadPipePID=$! + printlog "listening to output of curl with pipe $pipe and command file $DIALOG_CMD_FILE on PID $downloadPipePID" DEBUG + + curlDownload=$(curl -fL -# --show-error ${curlOptions} "$downloadURL" -o "$archiveName" 2>&1 | tee $pipe) + # because we are tee-ing the output, we want the pipe status of the first command in the chain, not the most recent one + curlDownloadStatus=$(echo $pipestatus[1]) + killProcess $downloadPipePID + + else + printlog "No Dialog connection, just download" DEBUG + curlDownload=$(curl -v -fsL --show-error ${curlOptions} "$downloadURL" -o "$archiveName" 2>&1) + curlDownloadStatus=$(echo $?) + fi + + deduplicatelogs "$curlDownload" + if [[ $curlDownloadStatus -ne 0 ]]; then + #if ! curl --location --fail --silent "$downloadURL" -o "$archiveName"; then + printlog "error downloading $downloadURL" ERROR + message="$name update/installation failed. This will be logged, so IT can follow up." + if [[ $currentUser != "loginwindow" && $NOTIFY == "all" ]]; then + printlog "notifying" + if [[ $updateDetected == "YES" ]]; then + displaynotification "$message" "Error updating $name" + else + displaynotification "$message" "Error installing $name" + fi + fi + printlog "File list: $(ls -lh "$archiveName")" ERROR + printlog "File type: $(file "$archiveName")" ERROR + cleanupAndExit 2 "Error downloading $downloadURL error:\n$logoutput" ERROR + fi + printlog "File list: $(ls -lh "$archiveName")" DEBUG + printlog "File type: $(file "$archiveName")" DEBUG + printlog "curl output was:\n$logoutput" DEBUG +fi + +# MARK: when user is logged in, and app is running, prompt user to quit app +if [[ $BLOCKING_PROCESS_ACTION == "ignore" ]]; then + printlog "ignoring blocking processes" +else + if [[ $currentUser != "loginwindow" ]]; then + if [[ ${#blockingProcesses} -gt 0 ]]; then + if [[ ${blockingProcesses[1]} != "NONE" ]]; then + checkRunningProcesses + fi + fi + fi +fi + +# MARK: install the download +printlog "Installing $name" REQ +if [[ $currentUser != "loginwindow" && $NOTIFY == "all" ]]; then + printlog "notifying" + if [[ $updateDetected == "YES" ]]; then + displaynotification "Updating $name" "Installation in progress …" + updateDialog "wait" "Updating..." + else + displaynotification "Installing $name" "Installation in progress …" + updateDialog "wait" "Installing..." + fi +fi + +if [ -n "$installerTool" ]; then + # installerTool defined, and we use that for installation + printlog "installerTool used: $installerTool" REQ + appName="$installerTool" +fi + +case $type in + dmg) + installFromDMG + ;; + pkg) + installFromPKG + ;; + zip) + installFromZIP + ;; + tbz|bz2) + installFromTBZ + ;; + pkgInDmg) + installPkgInDmg + ;; + pkgInZip) + installPkgInZip + ;; + appInDmgInZip) + installAppInDmgInZip + ;; + *) + cleanupAndExit 99 "Cannot handle type $type" ERROR + ;; +esac + +updateDialog "wait" "Finishing..." + +# MARK: Finishing — print installed application location and version +finishing + +# all done! +cleanupAndExit 0 "All done!" REQ