-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathnice_updater.sh
executable file
·269 lines (229 loc) · 14.4 KB
/
nice_updater.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#!/bin/bash
# shellcheck disable=SC2116,SC2002
# Written by Ryan Ball
# Obtained from https://github.com/ryangball/nice-updater
# These variables will be automagically updated if you run build.sh, no need to modify them
mainOnDemandDaemonPlist="/Library/LaunchDaemons/com.github.ryangball.nice_updater_on_demand.plist"
watchPathsPlist="/Library/Preferences/com.github.ryangball.nice_updater.trigger.plist"
preferenceFileFullPath="/Library/Preferences/com.github.ryangball.nice_updater.prefs.plist"
###### Variables in this section are specified in the preference file #####
updateInProgressTitle=$(defaults read "$preferenceFileFullPath" UpdateInProgressTitle)
updateInProgressMessage=$(defaults read "$preferenceFileFullPath" UpdateInProgressMessage)
notificationActionButtonText=$(defaults read "$preferenceFileFullPath" NotificationActionButtonText)
notificationOtherButtonText=$(defaults read "$preferenceFileFullPath" NotificationOtherButtonText)
log=$(defaults read "$preferenceFileFullPath" Log)
afterFullUpdateDelayDayCount=$(defaults read "$preferenceFileFullPath" AfterFullUpdateDelayDayCount)
afterEmptyUpdateDelayDayCount=$(defaults read "$preferenceFileFullPath" AfterEmptyUpdateDelayDayCount)
maxNotificationCount=$(defaults read "$preferenceFileFullPath" MaxNotificationCount)
yoPath=$(defaults read "$preferenceFileFullPath" YoPath)
###### Variables below this point are not intended to be modified #####
scriptName=$(/usr/bin/basename "$0")
osVersion=$(/usr/bin/sw_vers -productVersion)
osMinorVersion=$(/usr/bin/awk -F. '{print $2}' <<< "$osVersion")
osPatchVersion=$(/usr/bin/awk -F. '{print $3}' <<< "$osVersion")
restartEnabled="false"
automatedRestartEnabled="false"
# Determine icns path for softwareupdate
[[ "$osMinorVersion" -le 12 ]] && icon="/System/Library/CoreServices/Software Update.app/Contents/Resources/SoftwareUpdate.icns"
[[ "$osMinorVersion" -ge 13 ]] && icon="/System/Library/CoreServices/Install Command Line Developer Tools.app/Contents/Resources/SoftwareUpdate.icns"
# Determine if this version of macOS is compatable with the --restart argument for softwareupdate
[[ "$osMinorVersion" -eq 13 ]] && [[ "$osPatchVersion" -ge 4 ]] && automatedRestartEnabled="true"
[[ "$osMinorVersion" -ge 14 ]] && automatedRestartEnabled="true"
function writelog () {
DATE=$(date +%Y-%m-%d\ %H:%M:%S)
/bin/echo "${1}"
/bin/echo "$DATE" " $1" >> "$log"
}
function finish () {
# Record our last full update if we installed all updates
if [[ "$recordFullUpdate" == "true" ]]; then
writelog "Done with update process; recording last full update time."
/usr/libexec/PlistBuddy -c "Delete :last_full_update_time" $preferenceFileFullPath 2> /dev/null
/usr/libexec/PlistBuddy -c "Add :last_full_update_time string $(date +%Y-%m-%d\ %H:%M:%S)" $preferenceFileFullPath
writelog "Clearing user alert data."
/usr/libexec/PlistBuddy -c "Delete :users" $preferenceFileFullPath
writelog "Clearing On-Demand Update Key."
/usr/libexec/PlistBuddy -c "Delete :update_key" $preferenceFileFullPath 2> /dev/null
/usr/libexec/PlistBuddy -c "Add :update_key array" $preferenceFileFullPath 2> /dev/null
fi
kill "$jamfHelperPID" > /dev/null 2>&1 && wait $! > /dev/null
writelog "======== Finished $scriptName ========"
# If the updates installed require a restart, but the OS is 10.13.3 or lower, we need to manually restart the Mac
if [[ "$restartEnabled" == "true" ]] && [[ "$automatedRestartEnabled" == "false" ]]; then
writelog "Automated restart through softwareupdate only available in macOS 10.13.4 and above."
writelog "Initiating manual restart."
if [[ -f /usr/local/bin/jamf ]]; then
/usr/local/bin/jamf reboot -background -immediately | while read -r LINE; do writelog "$LINE"; done;
else
/sbin/shutdown -r now | while read -r LINE; do writelog "$LINE"; done;
fi
fi
}
# Make sure we run the finish function at the exit signal
trap 'finish' EXIT
function install_restart_updates () {
local restartArgument
writelog "Installing $updatesRestartCount update(s) that WILL REQUIRE a restart or shut down..."
recordFullUpdate="true"
restartEnabled="true"
# If the OS is 10.13.4 or higher, configure softwareupdate with both the --restart and --force arguments to automate the restart or shut down of the Mac
[[ "$automatedRestartEnabled" == "true" ]] && restartArgument='--restart --force'
/usr/sbin/softwareupdate --install --all --no-scan "$restartArgument" | /usr/bin/awk '!/Software Update Tool|Copyright|Finding|Done\.|^$/' | while read -r LINE; do writelog "$LINE"; done;
exit 0
}
function compare_date () {
then_unix="$(date -j -f "%Y-%m-%d %H:%M:%S" "$1" +%s)" # convert date to unix timestamp
now_unix="$(date +'%s')" # Get timestamp from right now
delta=$(( now_unix - then_unix )) # Will get the amount of time in seconds between then and now
daysAgo="$((delta / (60*60*24)))" # Converts the seconds to days
echo $daysAgo
return
}
function alert_user () {
local subtitle="$1"
[[ "$notificationsLeft" == "1" ]] && subtitle="1 remaining alert before auto-install."
[[ "$notificationsLeft" == "0" ]] && subtitle="Install now to avoid interruptions."
writelog "Stopping NiceUpdater On-Demand LaunchDaemon..."
/bin/launchctl unload -w "$mainOnDemandDaemonPlist"
writelog "Generating NiceUpdater Update Key..."
updateKey=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 16; echo)
/usr/bin/defaults write "$preferenceFileFullPath" update_key -array 2> /dev/null
/usr/bin/plutil -insert update_key.0 -string "$updateKey" "$preferenceFileFullPath"
/usr/libexec/PlistBuddy -c "Delete :update_key" $watchPathsPlist 2> /dev/null
writelog "Restarting NiceUpdater On-Demand LaunchDaemon..."
/bin/launchctl load -w "$mainOnDemandDaemonPlist"
writelog "Notifying $loggedInUser of available updates..."
/bin/launchctl asuser "$loggedInUID" "$yoPath" -t "Software Updates Required" -s "$subtitle" -n "Mac will restart after installation." \
-o "$notificationOtherButtonText" -b "$notificationActionButtonText" -B "/usr/bin/defaults write $watchPathsPlist update_key -string $updateKey" --ignores-do-not-disturb
/usr/libexec/PlistBuddy -c "Add :users dict" $preferenceFileFullPath 2> /dev/null
/usr/libexec/PlistBuddy -c "Delete :users:$loggedInUser" $preferenceFileFullPath 2> /dev/null
/usr/libexec/PlistBuddy -c "Add :users:$loggedInUser dict" $preferenceFileFullPath
/usr/libexec/PlistBuddy -c "Add :users:$loggedInUser:alert_count integer $2" $preferenceFileFullPath
}
function alert_logic () {
notificationCount=$(/usr/libexec/PlistBuddy -c "Print :users:$loggedInUser:alert_count" $preferenceFileFullPath 2> /dev/null | /usr/bin/xargs)
if [[ "$notificationCount" -ge "$maxNotificationCount" ]]; then
writelog "$loggedInUser has been notified $notificationCount times; not waiting any longer."
/Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -windowType utility -lockHUD -title "$updateInProgressTitle" -alignHeading center -alignDescription natural -description "$updateInProgressMessage" -icon "$icon" -iconSize 100 &
jamfHelperPID=$(echo $!)
install_restart_updates
else
((notificationCount++))
notificationsLeft="$((maxNotificationCount - notificationCount))"
alert_user "$notificationsLeft remaining alerts before auto-install." "$notificationCount"
fi
}
function update_check () {
writelog "Determining available Software Updates for macOS $osVersion..."
updates=$(/usr/sbin/softwareupdate -l)
# Account for the differences in pre/post Catalina softwareupdate -l output
if [[ "$osMinorVersion" -le 14 ]]; then
updatesNoRestart=$(echo "$updates" | /usr/bin/grep -Ei "restart|shut down" -B1 | /usr/bin/diff --normal - <(echo "$updates") | /usr/bin/sed -n -e 's/^.*[\*|-] //p')
updatesRestart=$(echo "$updates" | /usr/bin/grep -Ei "restart|shut down" -B1 | /usr/bin/grep -vEi 'restart|shut down' | /usr/bin/sed -n -e 's/^.*\* //p')
elif [[ "$osMinorVersion" -ge 15 ]]; then
updatesNoRestart=$(echo "$updates" | /usr/bin/grep -Ei -B1 "restart|shut down" | /usr/bin/diff - <(echo "$updates") | /usr/bin/sed -n 's/.*Label://p')
updatesRestart=$(echo "$updates" | /usr/bin/grep -Ei "restart|shut down" | /usr/bin/sed -e 's/.*Title: \(.*\), Ver.*/\1/')
fi
updatesNoRestartCount=$(echo -n "$updatesNoRestart" | /usr/bin/grep -c '^')
updatesRestartCount=$(echo -n "$updatesRestart" | /usr/bin/grep -c '^')
totalUpdateCount=$((updatesNoRestartCount + updatesRestartCount))
if [[ "$totalUpdateCount" -gt "0" ]]; then
# Download the updates
writelog "Downloading $totalUpdateCount update(s)..."
/usr/sbin/softwareupdate --download --all --no-scan | /usr/bin/awk '/Downloaded/{ print $0 }' | while read -r LINE; do writelog "$LINE"; done;
# Don't waste the user's time - install any updates that do not require a restart first.
if [[ -n "$updatesNoRestart" ]]; then
writelog "Installing $updatesNoRestartCount update(s) that WILL NOT require a restart in the background..."
# Loop through and install all of the updates that do not require a restart
while read -r LINE ; do
/usr/sbin/softwareupdate --install "$LINE" --no-scan | /usr/bin/awk '/Installing|Installed/{ print $0 }' | while read -r LINE; do writelog "$LINE"; done;
done < <(echo "$updatesNoRestart")
fi
# If the script moves past this point, a restart is required.
if [[ -n "$updatesRestart" ]]; then
writelog "A restart is required for remaining updates."
# If no user is logged in, just update and restart. Check the user now as some time has past since the script began.
loggedInUser=$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')
loggedInUID=$(id -u "$loggedInUser")
if [[ -z "$loggedInUser" ]]; then
writelog "No user logged in."
install_restart_updates
fi
# Getting here means a user is logged in, alert them that they will need to install and restart
alert_logic
else
recordFullUpdate="true"
writelog "No updates that require a restart available; exiting."
exit 0
fi
else
writelog "No updates at this time; exiting."
/usr/libexec/PlistBuddy -c "Delete :last_empty_update_time" $preferenceFileFullPath 2> /dev/null
/usr/libexec/PlistBuddy -c "Add :last_empty_update_time string $(date +%Y-%m-%d\ %H:%M:%S)" $preferenceFileFullPath
exit 0
fi
}
on_demand () {
# This function is intended to be run from a LaunchDaemon using WatchPaths that is triggered when the user
# clicks the "Install now" at a generated prompt. A randomized key is inserted simultaneously in both the
# WatchPaths file and a seperate preference file, and when confirmed for a match here, it is allowed to run.
# This elimantes any accidental runs if the WatchPaths file gets modified for any reason.
writelog " "
writelog "======== Starting $scriptName ========"
writelog "Verifying On-Demand Update Key..."
storedUpdateKeys=$(/usr/libexec/PlistBuddy -c "Print update_key" $preferenceFileFullPath | /usr/bin/sed -e 1d -e '$d' | /usr/bin/sed 's/^ *//')
testUpdateKey=$(/usr/bin/defaults read $watchPathsPlist update_key)
if [[ -n "$testUpdateKey" ]] && [[ "$storedUpdateKeys" == *"$testUpdateKey"* ]]; then
writelog "On-Demand Update Key confirmed; continuing."
/Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -windowType utility -lockHUD -title "$updateInProgressTitle" -alignHeading center -alignDescription natural -description "$updateInProgressMessage" -icon "$icon" -iconSize 100 &
jamfHelperPID=$(echo $!)
install_restart_updates
else
writelog "On-Demand Update Key not confirmed; exiting."
exit 0
fi
}
function main () {
# This function is intended to be run from a LaunchDaemon at intervals
writelog " "
writelog "======== Starting $scriptName ========"
# See if we are blocking updates, if so exit
updatesBlocked=$(/usr/libexec/PlistBuddy -c "Print :UpdatesBlocked" $preferenceFileFullPath 2> /dev/null | /usr/bin/xargs 2> /dev/null)
if [[ "$updatesBlocked" == "true" ]]; then
writelog "Updates are blocked for this client at this time; exiting."
exit 0
fi
# Check the last time we had a full successful update
updatesAvailable=$(/usr/bin/defaults read /Library/Preferences/com.apple.SoftwareUpdate.plist LastRecommendedUpdatesAvailable | /usr/bin/awk '{ if (NF > 2) {print $1 " " $2} else { print $0 }}')
if [[ "$updatesAvailable" -gt "0" ]]; then
writelog "At least one recommended update was marked available from a previous check."
update_check
else
lastFullUpdateTime=$(/usr/libexec/PlistBuddy -c "Print :last_full_update_time" $preferenceFileFullPath 2> /dev/null | /usr/bin/xargs 2> /dev/null)
lastEmptyUpdateTime=$(/usr/libexec/PlistBuddy -c "Print :last_empty_update_time" $preferenceFileFullPath 2> /dev/null | /usr/bin/xargs 2> /dev/null)
if [[ -n "$lastFullUpdateTime" ]]; then
daysSinceLastFullUpdate="$(compare_date "$lastFullUpdateTime")"
if [[ "$daysSinceLastFullUpdate" -ge "$afterFullUpdateDelayDayCount" ]]; then
writelog "$afterFullUpdateDelayDayCount or more days have passed since last full update."
update_check
else
writelog "Less than $afterFullUpdateDelayDayCount days since last full update; exiting."
exit 0
fi
elif [[ -n "$lastEmptyUpdateTime" ]]; then
daysSinceLastEmptyUpdate="$(compare_date "$lastEmptyUpdateTime")"
if [[ "$daysSinceLastEmptyUpdate" -ge "$afterEmptyUpdateDelayDayCount" ]]; then
writelog "$afterEmptyUpdateDelayDayCount or more days have passed since last empty update check."
update_check
else
writelog "Less than $afterEmptyUpdateDelayDayCount days since last empty update check; exiting."
exit 0
fi
else
writelog "This device might not have performed a full update yet."
update_check
fi
fi
exit 0
}
"$@"