├── README.md └── install-update-app-in-dmg.sh /README.md: -------------------------------------------------------------------------------- 1 | # install-update-app-in-dmg 2 | Script to Install or Update a macOS Application Inside a DMG 3 | 4 | The following script will allow you to ether install or update a macOS app that is packaged inside a dmg. This script will determine if the app is installed and in use, and if so, will give the user the opportunity to postpone or update the application. If the user chooses to update the application, it will close, update, then re-open for the user. However, if the installed app is the same version as what has been downloaded, the process will not continue as there is no update necessary. 5 | 6 | ## Requirements for Use Without Modification 7 | This script is intended to be used as a payload in a Jamf Pro policy but can easily be adapted to be used in other MDM environments. The prompting mechanism being used is jamfHelper, but this can be modified to use CocoaDialog without too much trouble. 8 | 9 | ## Script Parameters 10 | | Parameter | Variable Name | Description | 11 | | --------- | ------------- | ----------- | 12 | | 4 | downloadURL | Static url or command that can be used to return a dynamic url | 13 | | 5 | appFileName | Full name of the app; i.e. "Google Chrome.app" | 14 | | 6 | versionComparisonKey | Key in the app's Info.plist that you want to use to compare version; i.e. CFBundleShortVersionString | 15 | | 7 | commandToGetDownloadURL | (Optional) Specify whether the value in the downloadURL parameter is treated like command and evaluated ("true" OR empty) | -------------------------------------------------------------------------------- /install-update-app-in-dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | downloadURL="$4" # Static url or command that can be used to return a dynamic url 4 | appFileName="$5" # "Google Chrome.app" 5 | versionComparisonKey="$6" # CFBundleShortVersionString 6 | commandToGetDownloadURL="$7" # "true" or empty 7 | log="/var/log/install-upgrade-app-in-dmg.log" 8 | 9 | # This script is intended to be run to download a dmg containing a .app and either install 10 | # the .app or update the .app if a newer one was downloaded. 11 | 12 | ##### Variables beyond this point are not intended to be modified ##### 13 | appName="${appFileName%.*}" 14 | loggedInUser=$( echo "show State:/Users/ConsoleUser" | /usr/sbin/scutil | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }' ) 15 | loggedInUID=$(/usr/bin/id -u "$loggedInUser" 2> /dev/null) 16 | uuid=$(/usr/bin/uuidgen) 17 | workDir="/private/tmp/$uuid" 18 | newAppVersion="" 19 | appInstalled="false" 20 | mgmtAction="/Library/Application Support/JAMF/bin/Management Action.app/Contents/MacOS/Management Action" 21 | declare -a processIDs 22 | 23 | # Functions 24 | function writelog () { 25 | if [[ -n "$1" ]]; then 26 | DATE=$(date +%Y-%m-%d\ %H:%M:%S) 27 | printf "%s\n" "$1" 28 | printf "%s\n" "$DATE $1" >> "$log" 29 | else 30 | if test -t 0; then return; fi 31 | while IFS= read -r pipeData; do 32 | DATE=$(date +%Y-%m-%d\ %H:%M:%S) 33 | printf "%s\n" "$pipeData" 34 | printf "%s\n" "$DATE $pipeData" >> "$log" 35 | done 36 | fi 37 | } 38 | 39 | function clean_up () { 40 | writelog "Cleaning up installation files..." 41 | /usr/bin/hdiutil detach "$device" -force &> /dev/null 42 | /bin/rm -Rf "$workDir" 43 | 44 | if [[ "$appInstalled" == "true" ]]; then 45 | writelog "Updating inventory in Jamf Pro..." 46 | /usr/local/bin/jamf recon 47 | fi 48 | } 49 | 50 | function download_installation_files () { 51 | # Download the webpage source with installer download links 52 | writelog "$appName Download URL: $downloadURL" 53 | writelog "Downloading the $appName installation files..." 54 | 55 | # Exit if there was an error with the curl 56 | if ! /usr/bin/curl -s -L -f "$downloadURL" -o "$workDir/installMedium.dmg" ; then 57 | writelog "Error while downloading the installation files; exiting." 58 | exit 4 59 | fi 60 | 61 | # If no DMG was found in the install files, bail out 62 | if [[ ! -e "$workDir/installMedium.dmg" ]]; then 63 | writelog "Failed to download the installation files; exiting." 64 | exit 5 65 | fi 66 | 67 | # Mount the DMG, and save its device 68 | device=$(/usr/bin/hdiutil attach -nobrowse "$workDir/installMedium.dmg" | /usr/bin/grep "/Volumes" | /usr/bin/awk '{ print $1 }') 69 | if [[ -z "$device" ]]; then 70 | writelog "Failed to mount the downloaded dmg; exiting." 71 | exit 6 72 | fi 73 | 74 | # Using the device, determine the mount point 75 | mountPoint=$(/usr/bin/hdiutil info | /usr/bin/grep "^$device" | /usr/bin/cut -f 3) 76 | 77 | # Find the app inside the DMG 78 | downloadedApp=$(/usr/bin/find "$mountPoint" -type d -iname "*$appFileName" -maxdepth 1 | /usr/bin/grep -v "^$mountPoint$") 79 | 80 | # If no app was found in the dmg, bail out 81 | if [[ -z "$downloadedApp" ]]; then 82 | writelog "Failed to find $appFileName in downloaded installation files; exiting." 83 | exit 7 84 | fi 85 | 86 | # Extract the version of the newly downloaded app 87 | newAppVersion=$(/usr/bin/defaults read "$downloadedApp/Contents/Info.plist" $versionComparisonKey) 88 | } 89 | 90 | function check_if_downloaded_version_is_newer () { 91 | local oldAppVersion 92 | oldAppVersion=$(/usr/bin/defaults read "/Applications/$appFileName/Contents/Info.plist" $versionComparisonKey) 93 | 94 | if [[ -z "$newAppVersion" ]]; then 95 | writelog "Could not determine version of the downloaded $appFileName; exiting." 96 | exit 8 97 | fi 98 | 99 | # Robust version compare function came from: https://stackoverflow.com/a/4025065/12075814 100 | version_compare () { 101 | writelog "Comparing downloaded version with installed version..." 102 | if [[ "$1" == "$2" ]]; then 103 | return 0 104 | fi 105 | local IFS=. 106 | # shellcheck disable=SC2206 107 | local i ver1=($1) ver2=($2) 108 | # fill empty fields in ver1 with zeros 109 | for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do 110 | ver1[i]=0 111 | done 112 | for ((i=0; i<${#ver1[@]}; i++)); do 113 | if [[ -z ${ver2[i]} ]]; then 114 | # fill empty fields in ver2 with zeros 115 | ver2[i]=0 116 | fi 117 | if ((10#${ver1[i]} > 10#${ver2[i]})); then 118 | return 1 119 | fi 120 | if ((10#${ver1[i]} < 10#${ver2[i]})); then 121 | return 2 122 | fi 123 | done 124 | return 0 125 | } 126 | 127 | test_comparison () { 128 | version_compare "$1" "$2" 129 | case $? in 130 | 0) op='=';; 131 | 1) op='>';; 132 | 2) op='<';; 133 | esac 134 | writelog "Downloaded version ($1) $op Installed Version ($2)" 135 | if [[ "$op" != "$3" ]]; then 136 | writelog "Downloaded version IS NOT newer." 137 | return 1 138 | else 139 | writelog "Downloaded version IS newer." 140 | return 0 141 | fi 142 | 143 | } 144 | 145 | test_comparison "$newAppVersion" "$oldAppVersion" '>' 146 | 147 | return "$?" 148 | } 149 | 150 | function install_app () { 151 | writelog "Removing $appName..." 152 | /bin/rm -Rf "/Applications/$appFileName" 153 | writelog "Installing $appName..." 154 | /bin/cp -pR "$downloadedApp" /Applications/ 155 | 156 | if [[ -e "/Applications/$appFileName" ]]; then 157 | writelog "$appName installed successfully." 158 | appInstalled="true" 159 | else 160 | writelog "$appName installation failed; exiting." 161 | exit 9 162 | fi 163 | } 164 | 165 | function notify_user () { 166 | local counter iconPath title jamfHelper description iconName deferButton continueButton result buttonClicked 167 | 168 | counter="15" 169 | title="IT Notification - Update $appName" 170 | jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" 171 | deferButton="Later" 172 | continueButton="Update" 173 | description="$appName needs to be closed so it can be updated, please click \"$continueButton\" to close the application and update now or choose \"$deferButton\" if you are unable to update now." 174 | iconName=$(/usr/bin/defaults read "$downloadedApp/Contents/Info.plist" CFBundleIconFile) 175 | 176 | # Sometimes the app icon specified in Info.plist is like AppIcon.icns, and sometimes like AppIcon, so account for both 177 | if [[ "$iconName" == *.icns ]]; then 178 | iconPath="$downloadedApp/Contents/Resources/$iconName" 179 | else 180 | iconPath="$downloadedApp/Contents/Resources/$iconName.icns" 181 | fi 182 | 183 | result=$(/bin/launchctl asuser "$loggedInUID" "$jamfHelper" "$jamfHelper" -windowType "utility" -title "$title" -alignDescription natural -description "$description" -icon "$iconPath" -button1 "$continueButton" -button2 "$deferButton") 184 | buttonClicked="${result:$i-1}" 185 | 186 | # User clicked the button to continue 187 | if [[ "$buttonClicked" == "0" ]]; then 188 | writelog "$loggedInUser chose to continue, killing $appName processes..." 189 | 190 | # Populate array with the app's process IDs and kill them if there are more than 1 191 | processIDs=(); while IFS='' read -ra line; do processIDs+=("$line"); done < <(pgrep -x "$processName") 192 | [[ "${#processIDs[@]}" -ge "1" ]] && kill -9 "${processIDs[@]}" 193 | 194 | # If there are still active processes for the app, attempt to kill them every two seconds for 30 seconds 195 | while [[ "${#processIDs[@]}" -ge "1" ]] && [[ $counter -gt "0" ]]; do 196 | /bin/sleep 2 197 | processIDs=(); while IFS='' read -ra line; do processIDs+=("$line"); done < <(pgrep -x "$processName") 198 | [[ "${#processIDs[@]}" -ge "1" ]] && kill -9 "${processIDs[@]}" 199 | ((counter--)) 200 | done 201 | 202 | # If the processes are killed - install, otherwise exit 203 | if [[ "${#processIDs[@]}" -eq "0" ]]; then 204 | install_app 205 | writelog "Relaunching $appName for $loggedInUser." 206 | /bin/launchctl asuser "$loggedInUID" open "/Applications/$appFileName" 207 | else 208 | writelog "Could not kill $appName processes, exiting." 209 | exit 11 210 | fi 211 | 212 | # User clicked the defer button 213 | elif [[ "$buttonClicked" == "2" ]]; then 214 | writelog "$loggedInUser chose to postpone." 215 | exit 0 216 | 217 | # Something else occured 218 | else 219 | writelog "Unknown jamfHelper error occured; exiting." 220 | exit 12 221 | fi 222 | } 223 | 224 | # Main logic 225 | # Clean up our temporary files upon exiting at any time 226 | trap "clean_up" EXIT 227 | 228 | if [[ "$commandToGetDownloadURL" == "true" ]]; then 229 | writelog "Calculating Download URL from command." 230 | downloadURL="$(eval "$downloadURL")" 231 | fi 232 | 233 | # Look for missing required parameters and exit accordingly 234 | if [[ -z "$downloadURL" ]] || [[ "$downloadURL" != http* ]]; then 235 | writelog "Parameter 4 is missing or malformed; exiting." && exit 1 236 | fi 237 | [[ -z "$appFileName" ]] && writelog "Parameter 5 is missing; exiting." && exit 2 238 | [[ -z "$versionComparisonKey" ]] && writelog "Parameter 6 is missing; exiting." && exit 3 239 | 240 | # Make our working directory with our unique UUID generated in the variables section 241 | /bin/mkdir -p "$workDir" 242 | 243 | # Download the installation files 244 | download_installation_files 245 | 246 | # Check if the app is installed 247 | if [[ -e "/Applications/$appFileName" ]]; then 248 | writelog "$appName is installed; continuing." 249 | # If the installed version matches the downloaded version, bail out 250 | if ! check_if_downloaded_version_is_newer ; then 251 | writelog "The installed version of $appName is the latest version; exiting." 252 | exit 0 253 | else 254 | # The downloaded version is greater than the installed version, we need to update 255 | if [[ -z "$loggedInUser" ]]; then 256 | writelog "Nobody is logged in, performing unattended update..." 257 | install_app 258 | else 259 | writelog "$loggedInUser is logged in, determining the $appName process name..." 260 | 261 | # Extract the process name of the app from its Info.plist 262 | processName=$(/usr/bin/defaults read "/Applications/$appFileName/Contents/Info.plist" CFBundleExecutable) 263 | 264 | if [[ -z "$processName" ]]; then 265 | writelog "Could not determine process name of $appName; exiting." 266 | exit 10 267 | else 268 | writelog "Process name: $processName" 269 | fi 270 | 271 | writelog "Checking for running $appName processes..." 272 | 273 | processIDs=(); while IFS='' read -ra line; do processIDs+=("$line"); done < <(pgrep -x "$processName") 274 | if [[ "${#processIDs[@]}" -ge "1" ]]; then 275 | writelog "$appName is running, alerting $loggedInUser of pending update." 276 | notify_user 277 | else 278 | writelog "$appName is NOT running, installing..." 279 | install_app 280 | fi 281 | 282 | # The script would exit if the app was not installed, so inform the user via Notification Center 283 | /bin/launchctl asuser "$loggedInUID" "$mgmtAction" -title "$appName Updated" -message "$appName was successfully updated to version $newAppVersion." 284 | fi 285 | fi 286 | else 287 | writelog "$appName is NOT installed, performing installation." 288 | install_app 289 | 290 | # The script would exit if the app was not installed, so inform the user via Notification Center 291 | /bin/launchctl asuser "$loggedInUID" "$mgmtAction" -title "$appName Installed" -message "$appName version $newAppVersion was successfully installed." 292 | fi 293 | 294 | exit 0 --------------------------------------------------------------------------------