1#!/usr/bin/env bash
2
3################################################################################
4# Custom Functions (These functions may need to be edited for specific apps)
5################################################################################
6extract_latest_version(){
7 # used to extract the latest version from a website.
8 # /usr/bin/grep -Eo '^.{4}$' can be used to extract a version number of a specific length.
9 # generic extraction code: perl -pe 'if(($_)=/([0-9]+([.][0-9]+)+)/){$_.="
10"}' | /usr/bin/sort -Vu | /usr/bin/tail -n 1
11 perl -pe 'if(($_)=/([0-9]+([.][0-9]+)+)/){$_.="
12"}' | /usr/bin/sort -Vu | /usr/bin/grep -Eo '^.{5}$' | /usr/bin/tail -n 1
13}
14
15################################################################################
16# Fuctions (DO NOT EDIT THE BELOW FUNCTIONS, EXCEPT FOR MAIN)
17################################################################################
18message(){
19 # description of what function does.
20 local description='Displays a message to the customer if the defined application is running. Allow the customer to cancel if needed.'
21
22 # define local variables.
23 local message="${1}"
24 local title='IT Support'
25 local jamfHelper='/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
26 local cancelMessage='User chose to cancel the update the process.'
27
28 # data validation.
29 [[ -z "${applicationName}" ]] && applicationName='Application'
30 [[ -z "${applicationState}" ]] && applicationState=0
31 [[ -z "${message}" ]] && message='Press OK to continue'
32
33 # display dialog box message if application is running, otherwise continue silently.
34 if [[ -e "${jamfHelper}" && "${applicationState}" -eq 1 ]]; then
35 if ! "${jamfHelper}"
36 -windowType hud
37 -title "${title}"
38 -heading "${applicationName} Update"
39 -button1 'OK'
40 -button2 'Cancel'
41 -description "${message}"
42 -defaultButton 1
43 -lockHUD &>/dev/null
44 then
45 printf '%s
46' "ERROR: ${cancelMessage}" 1>&2
47 exit 1
48 fi
49 fi
50}
51
52error(){
53 # description of what function does.
54 local description='Displays an error message to the customer if the defined application is running. Otherwise prints to STDERR.'
55
56 # declare local variables.
57 local errorMessage="${1}"
58 local title='Block.one IT Support'
59 local jamfHelper='/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper'
60 local defaultMessage='Update Failed.'
61
62 # data validation
63 [[ -z "${applicationName}" ]] && applicationName='Application'
64 [[ -z "${applicationState}" ]] && applicationState=0
65 [[ -z "${errorMessage}" ]] && errorMessage='something went wrong. update failed.'
66
67 # display error message to customer only if application is running, otherwise print to STDERR.
68 if [[ -e "${jamfHelper}" && "${applicationState}" -eq 1 ]]; then
69 "${jamfHelper}"
70 -windowType hud
71 -title "${title}"
72 -heading "${applicationName} Update"
73 -button1 'OK'
74 -description "${defaultMessage}"
75 -defaultButton 1
76 -lockHUD &>/dev/null &
77 printf '%s
78' "ERROR: ${errorMessage}" 1>&2
79 exit 1
80 else
81 printf '%s
82' "ERROR: ${errorMessage}" 1>&2
83 exit 1
84 fi
85}
86
87get_installed_version(){
88 # description of what function does.
89 local description='Read and return version information from the Info.plist of the defined application.(aka: obtain version information for currently installed application.)'
90
91 # define local variables.
92 local applicationPath="${1}"
93 local installedVersion
94
95 # if the application path is defined and is a directory attempt to read and return version information
96 [[ -z "${applicationPath}" || ! -d "${applicationPath}" ]] && error 'application not installed or path undefined.'
97 installedVersion="$( /usr/bin/defaults read "${applicationPath}"/Contents/Info CFBundleShortVersionString 2> /dev/null )" || error 'could not detect installed version.'
98 [[ -z "${installedVersion}" ]] && error 'installed version undefined.'
99
100 # return installed version.
101 printf '%s
102' "${installedVersion}"
103}
104
105get_latest_version(){
106 # description of what function does.
107 local description='Extracts latest version information from a URL.'
108
109 # define local variables.
110 local latestVersionUrl="${1}"
111 local data
112 local latestVersion
113
114 # attempt to extract version information from URL and return value.
115 [[ -z "${latestVersionUrl}" ]] && error 'URL to search for latest version undefined.'
116 data="$( /usr/bin/curl -sLJ "${latestVersionUrl}" )" || error 'failed to download version data.'
117 [[ -z "${data}" ]] && error 'failed to download version data.'
118 latestVersion="$( printf '%s
119' "${data}" | extract_latest_version )" || error 'failed to extract latest version.'
120 [[ -z "${latestVersion}" ]] && error 'latest version undefined.'
121
122 # return latest version.
123 printf '%s
124' "${latestVersion}"
125}
126
127compare_versions(){
128 # description of what function does.
129 local description='Determines if the installed and latest versions are equal or not.'
130
131 # define local variables.
132 local latestVersion="${1}"
133 local installedVersion="${2}"
134
135 # data validation.
136 [[ -z "${latestVersion}" ]] && error 'latest version undefined.'
137 [[ -z "${installedVersion}" ]] && error 'installed version undefined.'
138
139 # use the sort commands built-in ability to sort version numbers.
140 if [[ "$( printf '%s
141' "${latestVersion}" "${installedVersion}" | /usr/bin/sort -V | /usr/bin/head -n 1 )" != "${latestVersion}" ]]; then
142 printf '%s
143' 'application needs to be updated.'
144 elif [[ "${latestVersion}" != "${installedVersion}" ]]; then
145 error 'installed version is newer. latest version URL may need to be updated.'
146 else
147 printf '%s
148' 'application is on the latest version.'
149 exit 0
150 fi
151}
152
153download(){
154 # description of what function does.
155 local description='Downloads a file from a given URL to a temporary directory and returns the full path to the download.'
156
157 # define local variables.
158 local dlURL="${1}"
159 dlDir=''
160 local dlName
161 local productVer
162 local userAgent
163 downloadPath=''
164
165 # if the download URL was provided. Build the effective URL (this helps if the given URL redirects to a specific download URL.)
166 [[ -z "${dlURL}" ]] && error 'download url undefined.'
167 dlURL="$( /usr/bin/curl "${dlURL}" -s -L -I -o /dev/null -w '%{url_effective}' )" || error 'failed to determine effective URL.'
168
169 # create temporary directory for the download.
170 dlDir="$( /usr/bin/mktemp -d 2> /dev/null )" || error 'failed to create temporary download directory.'
171 [[ ! -d "${dlDir}" ]] && error 'temporary download directory does not exist.'
172 export dlDir
173
174 # build user agent for curl.
175 productVer="$( /usr/bin/sw_vers -productVersion | /usr/bin/tr '.' '_' )" || error 'could not detect product version needed for user agent.'
176 [[ -z "${productVer}" ]] && error 'product version undefined'
177 userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X '"${productVer})"' AppleWebKit/535.6.2 (KHTML, like Gecko) Version/5.2 Safari/535.6.2'
178
179 # change the present working directory to the temporary download directory and attempt download.
180 cd "${dlDir}" || error 'could not change pwd to temporary download directory.'
181 dlName="$( /usr/bin/curl -sLJO -A "${userAgent}" -w "%{filename_effective}" --retry 10 "${dlURL}" )" || error 'failed to download latest version.'
182 [[ -z "${dlName}" ]] && error 'download filename undefined.'
183 downloadPath="${dlDir}/${dlName}"
184 [[ ! -e "${downloadPath}" ]] && error 'download filename undefined. can not locate download.'
185
186 # export full path to the downloaded file including extension.
187 export downloadPath
188}
189
190detect_running(){
191 # description of what function does.
192 local description='Detect if the defined application is currently running. Export the application state so it is available globally.'
193
194 # define variables.
195 applicationState=0
196
197 # determine if application is running and return result.
198 # 1 = running , 0 = not running.
199 if /usr/bin/pgrep -q "${applicationName}"; then
200 applicationState=1
201 export applicationState
202 else
203 export applicationState
204 fi
205}
206
207kill_running(){
208 # description of what function does.
209 local description='If the defined application is running. Give customer option to close it or cancel the update process.'
210
211 # notify customer and get input. attempt killing application if it is running and customer has agreed.
212 message "${applicationName} needs to be updated. The application will close if you continue."
213 if [[ "${applicationState}" -eq 1 ]]; then
214 /usr/bin/pkill -9 "${applicationName}" &>/dev/null
215 fi
216}
217
218uninstall(){
219 # description of what function does.
220 local description='Uninstalls the defined application.'
221
222 # define local variables.
223 local applicationPath="${1}"
224
225 # data validation.
226 [[ ! -d "${applicationPath}" ]] && error 'app path undefined or not a directory.'
227
228 # attempt uninstall.
229 /bin/mv "${applicationPath}" "${applicationPath}.old" &> /dev/null || error "failed to uninstall application."
230 sleep 2
231}
232
233install(){
234 # description of what function does.
235 local description='Determines what kind of installer the download is. Attempts install accordingly.'
236
237 # determine download installer type. (dmg, pkg, zip)
238 if [[ "$( printf '%s
239' "${downloadPath}" | /usr/bin/grep -c '.dmg$' )" -eq 1 ]]; then
240 install_dmg
241 elif [[ "$( printf '%s
242' "${downloadPath}" | /usr/bin/grep -c '.pkg$' )" -eq 1 ]]; then
243 install_pkg
244 elif [[ "$( printf '%s
245' "${downloadPath}" | /usr/bin/grep -c '.zip$' )" -eq 1 ]]; then
246 install_zip
247 else
248 error 'could not detect install type.'
249 fi
250}
251
252install_pkg(){
253 # description of what function does.
254 local description='Silently install pkg.'
255
256 # define local variables.
257 local pkg="${1}"
258
259 if [[ -z "${pkg}" ]]; then
260 pkg="${downloadPath}"
261 fi
262
263 # use installer command line tool to silently install pkg.
264 /usr/sbin/installer -allowUntrusted -pkg "${pkg}" -target / &> /dev/null || error 'failed to install latest version pkg.'
265}
266
267install_dmg(){
268 # description of what function does.
269 local description='Silently install dmg.'
270
271 # define variables.
272 mnt=''
273 local dmg="${1}"
274 local app
275 local pkg
276
277
278 if [[ -z "${dmg}" ]]; then
279 dmg="${downloadPath}"
280 fi
281
282 # create temporary mount directory for dmg and export path if exists.
283 mnt="$( /usr/bin/mktemp -d 2> /dev/null )" || error 'failed to create temporary mount point for dmg.'
284 [[ ! -d "${mnt}" ]] && error 'failed to verify temporary mount point for dmg exists.'
285 export mnt
286
287 # silently attach the dmg download to the temporary mount directory and determine what it contains (app or pkg)
288 sleep 2
289 /usr/bin/hdiutil attach "${dmg}" -quiet -nobrowse -mountpoint ${mnt} &> /dev/null || error 'failed to mount dmg.'
290 app="$( /bin/ls "${mnt}" | /usr/bin/grep '.app$' | head -n 1 )"
291 pkg="$( /bin/ls "${mnt}" | /usr/bin/grep '.pkg$' | head -n 1 )"
292
293 # attempt install based on contents of dmg.
294 if [[ ! -z "${app}" && -e "${mnt}/${app}" ]]; then
295 cp -Rf "${mnt}/${app}" '/Applications' &> /dev/null || error "failed to copy the latest version to the applications directory."
296 elif [[ ! -z "${pkg}" && -e "${mnt}/${pkg}" ]]; then
297 install_pkg "${mnt}/${pkg}"
298 else
299 error 'could not detect installation type in mounted dmg.'
300 fi
301}
302
303install_zip(){
304
305 # define variables.
306 uz=''
307 local app
308 local pkg
309 local dmg
310
311 # create temporary unzip directory and export globally if exists.
312 uz="$( /usr/bin/mktemp -d 2> /dev/null )" || error 'failed to create temporary unzip directory for zip.'
313 [[ ! -d "${uz}" ]] && error 'failed to verify temporary unzip directory exists.'
314 export uz
315
316 # unzip zip file and determine installer type. (app, pkg, dmg)
317 /usr/bin/unzip "${downloadPath}" -d "${uz}" &> /dev/null || error 'failed to unzip download.'
318 app="$( /bin/ls ${uz} | /usr/bin/grep '.app$' | head -n 1 )"
319 pkg="$( /bin/ls ${uz} | /usr/bin/grep '.pkg$' | head -n 1 )"
320 dmg="$( /bin/ls ${uz} | /usr/bin/grep '.dmg$' | head -n 1 )"
321
322 # attempt install based on contents of zip file.
323 if [[ ! -z "${app}" && -e "${uz}/${app}" ]]; then
324 cp -Rf "${uz}/${app}" '/Applications' &> /dev/null || error "failed to copy the latest version to the applications directory."
325 elif [[ ! -z "${pkg}" && -e "${uz}/${pkg}" ]]; then
326 install_pkg "${uz}/${pkg}"
327 elif [[ ! -z "${dmg}" && -e "${uz}/${dmg}" ]]; then
328 install_dmg "${uz}/${dmg}"
329 else
330 error 'could not detect installation type in unzipped download.'
331 fi
332}
333
334cleanup(){
335 # description of what function does.
336 local description='Removes temporary items created during the download and installation processes.'
337
338 local applicationPath="/Applications/${applicationName}.app"
339
340 # if a temporary mount directory has been created, force unmount and remove the directory.
341 if [[ -d "${mnt}" ]]; then
342 /usr/bin/hdiutil detach -force -quiet "${mnt}"
343 /sbin/umount -f "${mnt}" &> /dev/null
344 /bin/rm -rf "${mnt}" &> /dev/null
345 fi
346
347 # if temporary unzip directory exists, remove it.
348 if [[ -d "${uz}" ]]; then
349 /bin/rm -rf "${uz}" &> /dev/null
350 fi
351
352 # if the defined application does not exist restore the original to the apps directory.
353 if [[ ! -d "${applicationPath}" ]]; then
354 printf '%s
355' 'Update failed. Restoring original application...'
356 /bin/mv "${applicationPath}.old" "${applicationPath}" &> /dev/null
357 elif [[ -d "${applicationPath}.old" ]]; then
358 /bin/rm -rf "${applicationPath}.old" &> /dev/null
359 fi
360
361 # if a temporary download directory has been created. remove it.
362 if [[ -d "${dlDir}" ]]; then
363 /bin/rm -rf "${dlDir}" &> /dev/null
364 fi
365}
366
367main(){
368
369 # declare local variables
370 applicationName='Slack'
371 local latestVersionUrl='https://slack.com/downloads/mac'
372 local latestDownloadUrl='https://slack.com/ssb/download-osx'
373 local applicationPath="/Applications/${applicationName}.app"
374 local installedVersion
375 local latestVersion
376
377 # ensure cleanup runs on exit or error.
378 trap cleanup EXIT ERR
379
380 # export global variables
381 export applicationName
382
383 # determine if the application needs to be updated.
384 installedVersion="$( get_installed_version "${applicationPath}" )" || exit 1
385 latestVersion="$( get_latest_version "${latestVersionUrl}" )" || exit 1
386 compare_versions "${latestVersion}" "${installedVersion}"
387
388 # download latest version of the application and export full path to the temporary download location for the cleanup function.
389 download "${latestDownloadUrl}"
390
391 # determine if application is running and notify customer before attempting kill.
392 detect_running
393 kill_running
394
395 # uninstall the application if neeeded for the update.
396 uninstall "${applicationPath}"
397
398 # install latest version of the application.
399 install
400 message 'Update Successful'
401 exit 0
402}
403main "$@"