Skip to content

Commit

Permalink
Refactor to use automated restart (#9)
Browse files Browse the repository at this point in the history
* Refactor to use --restart argument for softwareupdate

* Fix merge

* Refactor to use --restart argument for softwareupdate
  • Loading branch information
ryangball authored Nov 24, 2019
1 parent 9f1ae69 commit 1c7e1f2
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 97 deletions.
Binary file modified com.github.ryangball.nice_updater.prefs.plist
Binary file not shown.
194 changes: 97 additions & 97 deletions nice_updater.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ mainOnDemandDaemonPlist="/Library/LaunchDaemons/com.github.ryangball.nice_update
watchPathsPlist="/Library/Preferences/com.github.ryangball.nice_updater.trigger.plist"
preferenceFileFullPath="/Library/Preferences/com.github.ryangball.nice_updater.prefs.plist"

###### Variables below this point are not intended to be modified #####
###### Variables in this section are specified in the preference file #####
updateInProgressTitle=$(defaults read "$preferenceFileFullPath" UpdateInProgressTitle)
updateInProgressMessage=$(defaults read "$preferenceFileFullPath" UpdateInProgressMessage)
loginAfterUpdatesInProgressMessage=$(defaults read "$preferenceFileFullPath" LoginAfterUpdatesInProgressMessage)
notificationActionButtonText=$(defaults read "$preferenceFileFullPath" NotificationActionButtonText)
notificationOtherButtonText=$(defaults read "$preferenceFileFullPath" NotificationOtherButtonText)
log=$(defaults read "$preferenceFileFullPath" Log)
Expand All @@ -22,64 +21,71 @@ maxNotificationCount=$(defaults read "$preferenceFileFullPath" MaxNotificationCo
yoPath=$(defaults read "$preferenceFileFullPath" YoPath)

###### Variables below this point are not intended to be modified #####
scriptName=$(basename "$0")
osVersion=$(sw_vers -productVersion)
osMinorVersion=$(echo "$osVersion" | awk -F. '{print $2}')
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 ========"
exit "$1"
}

function record_last_full_update () {
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
# 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
}

writelog "Clearing user alert data."
/usr/libexec/PlistBuddy -c "Delete :users" $preferenceFileFullPath
# Make sure we run the finish function at the exit signal
trap 'finish' EXIT

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
}
function install_restart_updates () {
local restartArgument
writelog "Installing $updatesRestartCount update(s) that WILL REQUIRE a restart or shut down..."
recordFullUpdate="true"
restartEnabled="true"

function initiate_restart () {
writelog "Initiating $restartType now..."
kill "$jamfHelperPID" > /dev/null 2>&1 && wait $! > /dev/null
if [[ "$restartType" = "restart" ]]; then
/usr/local/bin/jamf reboot -background -immediately | while read -r LINE; do writelog "$LINE"; done
finish 0
elif [[ "$restartType" = "shutdown" ]]; then
/sbin/halt | while read -r LINE; do writelog "$LINE"; done
finish 0
fi
}
# If the OS is 10.13.4 or higher, configure softwareupdate with the --restart argument to automate the restart or shut down of the Mac
[[ "$automatedRestartEnabled" == "true" ]] && restartArgument='--restart'

function trigger_updates () {
# Run softwareupdate and clean up the output so we only see what is necessary.
# 10.11 and above allows you to skip the update scan, so we can do that since we already scanned for updates initially
[[ "$osMinorVersion" -ge 11 ]] && noScan='--no-scan'
# shellcheck disable=SC2086
updateOutput=$(/usr/sbin/softwareupdate --install $1 "$noScan" | \
grep --line-buffered -v -E 'Software Update Tool|Copyright|Finding|Downloaded|Done\.|You have installed one|Please restart immediately\.|select Shut Down from the Apple menu|your computer must shut down|^$' | \
while read -r LINE; do writelog "$LINE"; done)
if [[ "$updateOutput" =~ "select Shut Down from the Apple menu" ]]; then
restartType="shutdown"
elif [[ "$updateOutput" =~ "your computer must shut down" ]]; then
restartType="shutdown"
else
restartType="restart"
fi
sleep 5
/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 () {
Expand All @@ -93,22 +99,21 @@ function compare_date () {

function alert_user () {
local subtitle="$1"
[[ "$notificationsLeft" == "1" ]] && local subtitle="1 remaining alert before auto-install."
[[ "$notificationsLeft" == "0" ]] && local subtitle="Install now to avoid interruptions."
[[ "$notificationsLeft" == "1" ]] && subtitle="1 remaining alert before auto-install."
[[ "$notificationsLeft" == "0" ]] && subtitle="Install now to avoid interruptions."

writelog "Stopping NiceUpdater On-Demand LaunchDaemon..."
launchctl unload -w "$mainOnDemandDaemonPlist"
/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/libexec/PlistBuddy -c "Add :update_key array" $preferenceFileFullPath 2> /dev/null
/usr/bin/plutil -insert update_key.0 -string "$updateKey" $preferenceFileFullPath
/usr/bin/defaults write "$preferenceFileFullPath" update_key -array 2> /dev/null
/usr/bin/plutil -insert update_key.0 -string "$updateKey" "$preferenceFileFullPath"

writelog "Clearing NiceUpdater On-Demand Trigger file..."
/usr/libexec/PlistBuddy -c "Delete :update_key" $watchPathsPlist 2> /dev/null

writelog "Restarting NiceUpdater On-Demand LaunchDaemon..."
launchctl load -w "$mainOnDemandDaemonPlist"
/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." \
Expand All @@ -120,15 +125,12 @@ function alert_user () {
}

function alert_logic () {
notificationCount=$(/usr/libexec/PlistBuddy -c "Print :users:$loggedInUser:alert_count" $preferenceFileFullPath 2> /dev/null | xargs)
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 $!)
writelog "Installing updates that DO require a restart..."
trigger_updates "--recommended"
record_last_full_update
initiate_restart
install_restart_updates
else
((notificationCount++))
notificationsLeft="$((maxNotificationCount - notificationCount))"
Expand All @@ -139,57 +141,58 @@ function alert_logic () {
function update_check () {
writelog "Determining available Software Updates for macOS $osVersion..."
updates=$(/usr/sbin/softwareupdate -l)
updatesNoRestart=$(echo "$updates" | /usr/bin/grep -v restart | /usr/bin/grep -B1 recommended | /usr/bin/grep -v recommended | /usr/bin/awk '{print $2}' | /usr/bin/awk '{printf "%s ", $0}')
updatesRestart=$(echo "$updates" | grep -i restart | grep -v '\*' | cut -d , -f 1)
updateCount=$(echo "$updates" | grep -i -c recommended)

if [[ "$updateCount" -gt "0" ]]; then
# 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 $updateCount update(s)..."
[[ "$osMinorVersion" -ge 11 ]] && noScan='--no-scan'
/usr/sbin/softwareupdate --download --recommended "$noScan" | grep --line-buffered Downloaded | while read -r LINE; do writelog "$LINE"; done
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 updates that DO NOT require a restart in the background..."
trigger_updates "$updatesNoRestart"
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/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");')
loggedInUser=$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }')
loggedInUID=$(id -u "$loggedInUser")
if [[ "$loggedInUser" == "root" ]] || [[ -z "$loggedInUser" ]]; then
if [[ -z "$loggedInUser" ]]; then
writelog "No user logged in."
writelog "Installing updates that DO require a restart..."
trigger_updates "--recommended"
record_last_full_update
# Some time has passed since we started to install the updates, check for a logged in user once more
loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");')
if [[ ! "$loggedInUser" == "root" ]] && [[ -n "$loggedInUser" ]]; then
writelog "$loggedInUser has logged in since we started to install updates, alerting them of pending restart."
/Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -windowType utility -lockHUD -title "$updateInProgressTitle" -alignHeading center -alignDescription natural -description "$loginAfterUpdatesInProgressMessage" -icon "$icon" -iconSize 100 -timeout "60"
initiate_restart
else
# Still nobody is logged in, restart
initiate_restart
fi
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
record_last_full_update
recordFullUpdate="true"
writelog "No updates that require a restart available; exiting."
finish 0
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
finish 0
exit 0
fi
}

Expand All @@ -202,19 +205,16 @@ on_demand () {
writelog " "
writelog "======== Starting $scriptName ========"
writelog "Verifying On-Demand Update Key..."
storedUpdateKeys=$(/usr/libexec/PlistBuddy -c "Print update_key" $preferenceFileFullPath | sed -e 1d -e '$d' | sed 's/^ *//')
testUpdateKey=$(defaults read $watchPathsPlist 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 $!)
writelog "Installing updates that DO require a restart..."
trigger_updates "--recommended"
record_last_full_update
initiate_restart
install_restart_updates
else
writelog "On-Demand Update Key not confirmed; exiting."
finish 0
exit 0
fi
}

Expand All @@ -225,10 +225,10 @@ function main () {
writelog "======== Starting $scriptName ========"

# See if we are blocking updates, if so exit
updatesBlocked=$(/usr/libexec/PlistBuddy -c "Print :UpdatesBlocked" $preferenceFileFullPath 2> /dev/null | xargs 2> /dev/null)
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."
finish 0
exit 0
fi

# Check the last time we had a full successful update
Expand All @@ -237,16 +237,16 @@ function main () {
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 | xargs 2> /dev/null)
lastEmptyUpdateTime=$(/usr/libexec/PlistBuddy -c "Print :last_empty_update_time" $preferenceFileFullPath 2> /dev/null | xargs 2> /dev/null)
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."
finish 0
exit 0
fi
elif [[ -n "$lastEmptyUpdateTime" ]]; then
daysSinceLastEmptyUpdate="$(compare_date "$lastEmptyUpdateTime")"
Expand All @@ -255,15 +255,15 @@ function main () {
update_check
else
writelog "Less than $afterEmptyUpdateDelayDayCount days since last empty update check; exiting."
finish 0
exit 0
fi
else
writelog "This device might not have performed a full update yet."
update_check
fi
fi

finish 0
exit 0
}

"$@"

0 comments on commit 1c7e1f2

Please sign in to comment.