diff --git a/com.github.ryangball.nice_updater.prefs.plist b/com.github.ryangball.nice_updater.prefs.plist index 88b4c55..ec7cb0d 100644 Binary files a/com.github.ryangball.nice_updater.prefs.plist and b/com.github.ryangball.nice_updater.prefs.plist differ diff --git a/nice_updater.sh b/nice_updater.sh index 718c2cc..7d1d7e2 100755 --- a/nice_updater.sh +++ b/nice_updater.sh @@ -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) @@ -22,12 +21,21 @@ 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}" @@ -35,51 +43,49 @@ function writelog () { } 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 () { @@ -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." \ @@ -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))" @@ -139,20 +141,33 @@ 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. @@ -160,36 +175,24 @@ function update_check () { 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 } @@ -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 } @@ -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 @@ -237,8 +237,8 @@ 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 @@ -246,7 +246,7 @@ function main () { 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")" @@ -255,7 +255,7 @@ 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." @@ -263,7 +263,7 @@ function main () { fi fi - finish 0 + exit 0 } "$@"