Custom Patch Management Workflow

kendalljjohnson
Contributor II

With the goal of having better user interaction options that what is provided within Jamf's Patch Management Policies (not having the app quit 5 minutes later without warning) I have been testing different options with my existing update workflow and open source tools like Yo. I ended up using Alerter which will wait for user interaction within a script and allows the calling of a command with sudo permissions in order to execute a policy trigger.

28a985a9c48144409183df00b389e93b

I haven't deployed it to my fleet yet but figured I would see what feedback the community might have. It takes a 2 policies and 1 smart group per software title, which is a bit cumbersome, and currently never forces the update as the user could infinitely defer the installation if the app is never quit. I use a deferral counter for macOS software updates that could be integrated but I have not taken the time to do so yet.

I'm open to any and all feedback, thanks!

Setup Prior to Workflow

Script
By setting up the Script Parameters it can be adapted to any title

#!/bin/bash


# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation, 
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting


# $4 = Title
# $5 = ParameterID
# $6 = Process Name
# $7 = Jamf Policy Event
# $8 = App Path

# Define currently logged in user
currentUser="$(ls -la /dev/console | cut -d " " -f 4)"

# Look if app is open via process name
appOpen="$(pgrep -ix "$6" | wc -l)"


if [[ $appOpen -gt 0 ]]; then
        updateAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender $5 -message "must be quit in order to update. Save all data before quitting." -closeLabel Later -actions "Quit & Update")"
            if [[ $updateAnswer == "Quit & Update" ]]; then
                    sudo -u $currentUser killall "$6"
                    jamf policy -event "$7"
                    reopenAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated. Thank you." -closeLabel Ok -actions Reopen -timeout 60)"
                        if [[ $reopenAnswer == Reopen ]]; then
                                sudo -u $currentUser open "$8"
                        fi
            else
                    echo "User deferred"
            fi
    else
        jamf policy -event "$7"
fi

Policy
1. Create package that installs the alerter executable to /Library/Application Support/JAMF/
2. Push pkg via policy to install Alerter pkg to All Manage Clients (or whatever scope will utilize this workflow)

Patch Management Title
Create a Patch Management Title for 'Google Chrome'. If the app you are looking to update is not available as a patch Management Title setup your smart group to have Application Version instead of Patch Reporting.

Testing Rings
To allow the update to be pushed out in rings for testing, pre-defined Static/Smart Groups are setup with the following logic:

Ring 1 - Smart Group based IT computers
Ring 2 - Static Group of manually selected group with various roles and use cases across organization, around 15-20% of computer count
Ring 3 - all computers not in Ring 1 or Ring 2

Workflow Setup

Smart Computer Group
Create a Smart Computer Group to scope to any computer with the app installed and not running the desired version
1) Smart Group name: *Google Chrome Smart Update (Asterisk to keep Smart Update groups grouped at the top of your Smart Computer Group list)
2) Application Title is Google Chrome.app
3) Patch Reporting: Google Chrome is not 65.0.3325.181

Policies
Create a Policy used to trigger the installation of the pkg
1) Policy name: Install Latest Google Chrome Trigger
2) Custom Trigger: install_googleChromeUpdate
3) Execution Frequency: Ongoing
4) Packages: GoogleChrome-65.0.3325.181.pkg
5) Scope: *Google Chrome Smart Update
6) Maintenance: Update Inventory

Create policy that will prompt user if app is open, install if it is not open
1) Policy Name: Install Latest Google Chrome Alerter
2) Scripts: Alerter App Updates
3) Parameter Values
a. Title: Google Chrome
b. Sender ID: com.google.chrome c. Process Name: Google Chrome d. Jamf Policy Event: install_googleChromeUpdate e. App Path: /Applications/Google Chrome.app
4) Scope:
a. Target: *Google Chrome Smart Update
b. Exclusions: Ring 1, Ring 2, Ring 3

Ongoing Maintenance
When a new update is release the following maintenance would be required
1) Upload new pkg release for Google Chrome
2) Change Smart Computer Group the Patch Reporting version to new release number
3) Update the pkg within Install Latest Google Chrome Trigger policy
4) Reset Ring Exclusions - Based on testing schedule remove a group from the exclusions as needed (start with Ring 2 and Ring 3 as excluded, one week later remove Ring 2, one week after that remove Ring 3)

1 ACCEPTED SOLUTION

kendalljjohnson
Contributor II

Realized I never posted what I finally settled on and put in production in case others want to give it a try. Been running with it for a while and seems to be working pretty well! Thanks for all the input and testing from others in the thread.

#!/bin/bash


# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation,
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting
# Quit and Open path have 2 entries for the times you are quiting/uninstalling an old version of an app that is replaced by a new name (for example quiting Adobe Acrobat Pro, which is replaced by Adobe Acorbat.app)

################################DEFINE VARIABLES################################

# $4 = Title
# $5 = App ID
# $6 = Process Name
# $7 = Jamf Policy Event
# $8 = Quit App Path
# $9 = Open App Path

#Defining the Sender ID as self service due to setting the Sender ID as the actual app being updated would often cause the app to crash
sender="com.jamfsoftware.selfservice.mac"
#Jamf parameters can't be passed into a function, redefining the app path to be used within the funciton
quitPath="$8"
openPath="$9"

################################SETUP FUNCTIONS TO CALL################################

fGetCurrenUser (){
currentUser=`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 + "
");'`

  # Identify the UID of the logged-in user
  currentUserUID=`id -u "$currentUser"`
}

fQuitApp (){
cat > /private/tmp/quit_application.sh <<EOF
#!/bin/bash

/bin/launchctl asuser "$currentUserUID" /usr/bin/osascript -e 'tell application "$quitPath" to quit'
EOF

/bin/chmod +x /private/tmp/quit_application.sh
/bin/launchctl asuser "$currentUserUID" sudo -iu "$currentUser" "/private/tmp/quit_application.sh"
/bin/rm -f "/private/tmp/quit_application.sh"
}

fOpenApp (){
  cat > /private/tmp/open_application.sh <<EOF
#!/bin/bash

/usr/bin/open "$openPath"
EOF

/bin/chmod +x /private/tmp/open_application.sh
/bin/launchctl asuser "$currentUserUID" sudo -iu "$currentUser" "/private/tmp/open_application.sh"
/bin/rm -f "/private/tmp/open_application.sh"
}

################################SETUP TIMER FILE################################

## Set up the software update time if it does not exist already
if [ ! -e /Library/Application Support/JAMF/.$5.timer.txt ]; then
  echo "2" > /Library/Application Support/JAMF/.$5.timer.txt
fi

## Get the timer value
timer=`cat /Library/Application Support/JAMF/.$5.timer.txt`

################################ALERTER MESSAGE OPTIONS################################

saveQuitMSG="must be quit in order to update. Save all data before quitting."
updatedMSG="has been updated. Thank you."

################################START 'UPDATE WITH ALERTER' PROCESS################################

# Look if app is open via process name
appOpen="$(pgrep -ix "$6" | wc -l)"

# if the app is open and the defer timer is not zero
if [[ $appOpen -gt 0 && $timer -gt 0 ]]; then
    fGetCurrenUser
    updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$saveQuitMSG" -closeLabel "Defer ($timer)" -actions "Quit & Update" -timeout 3600)"
    if [[ $updateAnswer == "Quit & Update" ]]; then
        #quit app, install the update, then prompt the user when complete and ask if they want to reopen the app. Message will time out after 60 secs.
        fQuitApp
        /usr/local/bin/jamf policy -event "$7"
        reopenAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$updatedMSG" -closeLabel Ok -actions Reopen -timeout 60)"
        if [[ $reopenAnswer == Reopen ]]; then
            fOpenApp
        fi
        #reset timer after updating
        echo "2" > /Library/Application Support/JAMF/.$5.timer.txt

    else
        let CurrTimer=$timer-1
        echo "User chose to defer"
        echo "$CurrTimer" > /Library/Application Support/JAMF/.$5.timer.txt
        echo "Defer count is now $CurrTimer"
        exit 0
    fi
# if app is open and defer timer has run out
elif [[ $appOpen -gt 0 && $timer == 0 ]]; then
    fGetCurrenUser
    /bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$saveQuitMSG" -actions "Quit & Update" -closeLabel "No Deferrals Left " -timeout 3600
    fQuitApp
    /usr/local/bin/jamf policy -event "$7"
    reopenAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$updatedMSG" -closeLabel Ok -actions Reopen -timeout 60)"
    if [[ $reopenAnswer == Reopen ]]; then
        fOpenApp
    fi
    #reset timer after updating
    echo "2" > /Library/Application Support/JAMF/.$5.timer.txt

else
    # app is not open, reset timer and run updates
    echo "2" > /Library/Application Support/JAMF/.$5.timer.txt
    /usr/local/bin/jamf policy -event "$7"
fi

View solution in original post

112 REPLIES 112

preinheimer
New Contributor II

Hi,

nice workflow. But can you explain $5 = ParameterID more in detail? Where do you get the ParameterID?

Thanks, Philipp

remyb
Contributor

This is very interesting, I have a similar workflow for patching, that just checks if the application is running and doesn't do anything if it it. I might replace that part of the workflow with this!

@preinheimer The ParamaterID will be the '-sender' argument for Alerter

-sender ID         The bundle identifier of the application that should be shown as the sender, including its icon.

mottertektura
Contributor

@kendalljjohnson This is awesome! Thanks so much for your hard work on this!

This is very close to what we've been doing with @kitzy's Better Software Updates with End User Interaction script that we're currently using in our environment.

But I really like the idea of using notifications for this instead, especially since folks tend to trust and respond to those a lot more these days. What's nice about the Better Software Updates... script is that also allows for multiple processes (comma separated) to be closed if needed. I've modified the script you've shared to behave similarly and thought I'd post if anyone else might find it useful.

#!/bin/bash

# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation, 
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting

# $4 = Title
# $5 = ParameterID
processNames="${6}"
# $7 = Jamf Policy Event
# $8 = App Path

IFS="," # Set internal field separator to comma to saparate process names

# Identify the username of the logged-in user
currentUser=`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 + "
");'`

# Identify the UID of the logged-in user
currentUserUID=`id -u "$currentUser"`

for process in $processNames
do

PID="" # Clear PID to elimnate false positives

PID=`pgrep "$process"` # Get application PID

if [ ! -z "$PID" ] # Detect if application is running
    then
        # Prompt user to quit the running application
        echo "$process is running, prompting user to quit"
        updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "$process must close to install an update." -closeLabel Later -actions "OK" -timeout 300)"
        if [[ $updateAnswer == "OK" ]]
            then
                echo "User clicked OK"
                # Ask application to quit
                osascript -e "tell application "$process" to quit"
        else
            echo "User clicked Later. Exiting..."
            exit 0
        fi
    else
        echo "$process not running, moving on..."
fi

done

jamf policy -event "$7"
jamf recon

reopenAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated." -closeLabel OK -actions Open -timeout 60)"
if [[ $reopenAnswer == Open ]]; then
    sudo -u $currentUser open "$8"
    echo "Opening $8 for $currentUser..."
fi

P.S. If anyone catches any mistakes or has any suggestions for improvements please let me know. Thanks!

remyb
Contributor

Has anyone tested this in a policy so far? Running alerter as normal user seems to work, but if it runs in a script that runs as root, it fails to connect:

2018-04-17 09:27:53.661 alerter[2252:16472] NSNotificationCenter connection invalid
2018-04-17 09:27:53.661 alerter[2252:16472] Connection to notification center invalid. ServerConnectionFailure: 1 invalidated: 0

mottertektura
Contributor

@remyb, hmmm I've tested both scripts using a policy and didn't have any issues running as root. Are you able to run Alerter in terminal as root?

/Library/Application Support/JAMF/alerter -message 'Start now ?' -closeLabel No -actions YES,MAYBE,'one more action' -timeout 10

remyb
Contributor

@mottertektura I believe I found the issue; I'm changing to root using: sudo su -, this discards the current environment. Changing to root using: sudo su (without the -) does work and now I have working notifications. Sorry for the confusion!

crogersgrazado
New Contributor II

Hi there @kendalljjohnson, thanks for creating this! It looks like it would work very elegantly and I've begun experimenting with it.

I noticed in your writeup that there isn't a policy trigger specified for the "Install Latest Google Chrome Alerter." Would it be recurring check-in?

Also, @mottertektura , I think I had a similar issue running the policy(?)... the script attempted to run but just got hung up somewhere until I cancelled it. Not sure yet where I'm running into problems yet.

kendalljjohnson
Contributor II

@crogersgrazado You're right, looks like I totally skipped that. I've been testing it with recurring. The frequency would probably need to be set to your environment's urgency. It would need to be either an ongoing or once per day/week/month, depending on how much you want to bug your user. The alerts would not build up, since the script awaits user interaction. But if you have it set to ongoing, with the default every 15 minutes for check-in, then if your user is at their computer and says "Later" every time, they will repeatedly get peppered with the alert throughout the day. Since there's isn't a force install feature built in (yet?), if they are only asked once a day or week it could be an easy enough process that they click "Later" every time and never actually run the update. Whatever the time frame you set, they would not stop getting the alert until they actually quit the app, which allows the install to happen, which would include the recon after so they get moved out of the smart group saying they weren't on the desired version.

As far as the error, I'm not sure as I haven't run into that specific issue. You can always play with it as a local script that you can call from an elevated account, that might give more feedback or flexibility in testing. Even try each line manually, run within a terminal window with sudo permissions while logged into a non-admin account.

Hope that helps, glad ya'll are getting some use out of this.

crogersgrazado
New Contributor II

@kendalljjohnson Thanks so much for the thorough response - I figured that was the case.

As for the issue I'm having, I probably missed a small detail somewhere. I'll do more testing and report back, and do keep us posted if you roll it out! Thanks again!

remyb
Contributor

It might be necessary to add the timeout option to alerter if you use this in production. If the user never presses any button, the policy will be stuck waiting for user input. And the machine will stop checking in until the user actually provides that input.

mottertektura
Contributor

Found out that if I manually run jamf policy on the client, the policy works as expected and I receive the notification to update, but if I let the policy run with a normal check in, in hangs and never sends the notification. @crogersgrazado @remyb Is this what you were running into as well? I also agree with @remyb , it would probably be a good idea to add a timeout to the update notification so it doesn't hang there and cause the client to stop checking in.

remyb
Contributor

It wasn't the issue I was originally facing, but after some further testing today I ran into the same issue you are now describing. Running a manual sudo jamf policy results in a notification, letting the check-in handle it doesn't produce a notification and 'ps -ef|grep jamf' shows that it hangs.

kendalljjohnson
Contributor II

@remyb @mottertektura @crogersgrazado

Great catch by you all! I had only tested by manually triggering the policy check, not with the recurring checkin. I am getting the same results.

I started a support case, as there is some weird difference between how a policy is called when running "sudo jamf policy" and a recurring check-in. My initial thoughts is there would be a difference in permissions between the elevated terminal command with sudo and our jamf management account using the launch agents/daemons for check ins.

I'll let you know if I get any news from support on how to address this difference!

mm2270
Legendary Contributor III

@kendalljjohnson what you're experiencing is normal, sort of. The difference is that when you do sudo jamf policy, you're actually running the process as you, but with elevated privileges. So when something like Alerter for example, requires running as the user to work, it works ok when the policy is manually called, because again, it's really you running it with elevated rights.
When the recurring trigger runs, it gets called by the Jamf LaunchDaemon, which runs completely as root, meaning tools like Alerter won't work. This is partly Apple to blame as they keep tightening the screws on security around what can display messages to the user, especially interactive stuff. I've run into the same issue with Yo, for example.

The way I've gotten around it is to use the /bin/launchctl asuser syntax. If you do searches on that you'll find examples on how to use it to get this to work. Again this only typically applies when the LaunchDaemon calls the policy. It may not apply with manually called policies, Self Service policies and so on.

kendalljjohnson
Contributor II

@remyb @mottertektura @crogersgrazado

Once again, @mm2270 saves the day with scripting. Found the right method of getting the correct user info from a Rich Trouton blog post and was able to get the policy to run correctly from a recurring check-in. Try the following script, which calls Alerter with launchctl asuser. I also moved the part of getting the currentUser info inside the then statement since that information is only needed if the app is open, rather than getting the info every time the script is run.

#!/bin/sh


# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation, 
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting


# $4 = Title
# $5 = ParameterID
# $6 = Process Name
# $7 = Jamf Policy Event
# $8 = App Path

# Look if app is open via process name
appOpen="$(pgrep -ix "$6" | wc -l)"


if [[ $appOpen -gt 0 ]]; then
    # Identify the username of the logged-in user
    currentUser=`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 + "
");'`

    # Identify the UID of the logged-in user
    currentUserUID=`id -u "$currentUser"`

    updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender $5 -message "must be quit in order to update. Save all data before quitting." -closeLabel Later -actions "Quit & Update")"
            if [[ $updateAnswer == "Quit & Update" ]]; then
                    sudo -u $currentUser killall "$6"
                    jamf policy -event "$7"
                    reopenAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated. Thank you." -closeLabel Ok -actions Reopen -timeout 60)"
                        if [[ $reopenAnswer == Reopen ]]; then
                                sudo -u $currentUser open "$8"
                        fi
            else
                    echo "User deferred"
            fi
    else
        jamf policy -event "$7"
fi

mottertektura
Contributor

@kendalljjohnson , Thanks! I updated the version I posted above as well with this change. I thought about trying to adjust as well to only re-open the app if it was running, but because I'm checking for multiple processes I couldn't add it inside that then statement or it could potentially open multiple times. Thanks again!

FritzsCorner
Contributor III

@kendalljjohnson

Thanks for the script! I took a very crude stab at adding some minimal deferral functionality. I added a few options to explicitly define the max number of deferrals as well as where to put the deferral text file. Additionally, I use the app name for the deferral text file in the event there are multiple apps that need to be updated.

I was thinking about adding a timer to the alert so in the event a user is logged on but doesn't respond within a certain time-frame it will automatically close with the Defer option. When the user runs out of deferrals I was looking at adding a warning notification 5 minutes before the app is updated.

#!/bin/bash

# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation, 
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting


# $4 = Title
# $5 = ParameterID
# $6 = Process Name
# $7 = Jamf Policy Event
# $8 = App Path
# $9 = Deferral Count
# $10 = Deferral Timer Path

######### Set variables for the script ############

## Set the Patch Update Timer if it does not exist already
appTimer="${10}.${4}PatchTimer.txt"
if [ ! -e "$appTimer" ]; then
        echo "$9" >"$appTimer"
fi

## Get the timer value
Timer=`cat "$appTimer"`

# Alerter Messages
appMsg="Mandatory $4 Update"
deferralMsg="Save all data before continuing. You have $Timer deferalls remaining."

# Identify the username of the logged-in user
currentUser=`who | grep console | awk '{print $1}'`

# Identify the UID of the logged-in user
currentUserUID=`id -u "$currentUser"`

################ End Variable Set ################

## if there is no one logged in, just run the update
if [ "$currentUser" == "" ]; then
    echo "No one is logged in. Continuing Install"
    jamf policy -event "$7"
    echo "The install was successfull, removing deferral Timer"
    rm -rf $appTimer
    exit 0
else
    echo "A user is logged in"
fi

# Look if app is open via process name
appOpen="$(pgrep -ix "$6" | wc -l)"


if [[ $appOpen -gt 0 ]]; then
    echo "The application is open. Checking Deferral count"
    if [ $Timer -gt 0 ]; then
        updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$appMsg" -sender $5 -message "$deferralMsg" -closeLabel Later -actions "Quit & Update")"

        if [[ $updateAnswer == "Quit & Update" ]]; then
            sudo -u $currentUser killall "$6"
            jamf policy -event "$7"
            reopenAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated. Thank you." -closeLabel Ok -actions Reopen -timeout 60)"

            if [[ $reopenAnswer == Reopen ]]; then
                sudo -u $currentUser open "$8"
            fi

        else
                let CurrTimer=$Timer-1
                echo "User chose to defer."
                echo "$CurrTimer" > $appTimer
                exit 0
        fi

    else
        echo "No more Deferals Allowed"
        sudo -u $currentUser killall "$6"
        jamf policy -event "$7"
        echo "The install was successfull, removing deferral Timer"
        rm -rf $appTimer
        reopenAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated. Thank you." -closeLabel Ok -actions Reopen -timeout 60)"
        if [[ $reopenAnswer == Reopen ]]; then
            sudo -u $currentUser open "$8"
        fi
    fi  
else
    echo "App is not in use. Continuing with the update."
    jamf policy -event "$7"
    echo "The install was successfull, removing deferral Timer"
    rm -rf $appTimer
    exit 0
fi

exit 0

kendalljjohnson
Contributor II

@FritzsCorner Very cool! Oddly enough, I was working on the same thing yesterday and we came to pretty similar results. One difference I added was a warning once they are out of deferrals so the app doesn't quit on them out of nowhere. I also added a 4 hour timeout on each alert so that other policies would eventually continue if the user never acknowledges.

#!/bin/sh


# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation,
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting


# $4 = Title
# $5 = Sender ID
# $6 = Process Name
# $7 = Jamf Policy Event
# $8 = App Path


## Set up the software update time if it does not exist already
if [ ! -e /Library/Application Support/JAMF/.$5.timer.txt ]; then
    echo "2" > /Library/Application Support/JAMF/.$5.timer.txt
fi

## Get the timer value
timer=`cat /Library/Application Support/JAMF/.$5.timer.txt`

# Look if app is open via process name
appOpen="$(pgrep -ix "$6" | wc -l)"

# if the app is open and the defer timer is not zero
if [[ $appOpen -gt 0 && $timer -gt 0 ]]; then
    # Identify the username of the logged-in user
    currentUser=`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 + "
");'`

    # Identify the UID of the logged-in user
    currentUserUID=`id -u "$currentUser"`

    updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender $5 -message "must be quit in order to update. Save all data before quitting." -closeLabel "Defer ($timer)" -actions "Quit & Update" -timeout 14400)"
            if [[ $updateAnswer == "Quit & Update" ]]; then
              #quit app, install the update, then prompt the user when complete and ask if they want to reopen the app. Message will time out after 60 secs.
              sudo -u $currentUser killall "$6"
              jamf policy -event "$7"
              reopenAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated. Thank you." -closeLabel Ok -actions Reopen -timeout 60)"
                  if [[ $reopenAnswer == Reopen ]]; then
                          sudo -u $currentUser open "$8"
                  fi
              #reset timer after updating
              echo "2" > /Library/Application Support/JAMF/.$5.timer.txt

            else
                    let CurrTimer=$timer-1
                    echo "User chose to defer"
                    echo "$CurrTimer" > /Library/Application Support/JAMF/.$5.timer.txt
                    echo "$CurrTimer"
                    exit 0
            fi
# if app is open and defer timer has run out
elif [[ $appOpen -gt 0 && $timer == 0 ]]; then
    currentUser=`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 + "
");'`

    # Identify the UID of the logged-in user
    currentUserUID=`id -u "$currentUser"`

    /bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender $5 -message "must be quit in order to update. Save all data." -actions "Quit & Update" -closeLabel "No Deferrals Left " -timeout 14400

    # whether user quits, clicks on no deferrals, or the message times out: quit and update 
    # quit app, install the update, then prompt the user when complete and ask if they want to reopen the app. Message will time out after 60 secs.
    sudo -u $currentUser killall "$6"
    jamf policy -event "$7"
    reopenAnswer="$(/Library/Application Support/JAMF/alerter -title "$4" -sender "$5" -message "has been updated. Thank you." -closeLabel Ok -actions Reopen -timeout 60)"
        if [[ $reopenAnswer == Reopen ]]; then
            sudo -u $currentUser open "$8"
        fi
    #reset timer after updating
    echo "2" > /Library/Application Support/JAMF/.$5.timer.txt

else
        # app is not open, reset timer and run updates
        echo "2" > /Library/Application Support/JAMF/.$5.timer.txt
        jamf policy -event "$7"
fi

remyb
Contributor

I went a bit further in testing and adapting this into a more universal usable script that you can re-use in multiple policies

#!/bin/bash

# $4 = Sender bundle id
# $5 = Notification Title
# $6 = Notification message
# $7 = Cancel button
# $8 = Run event button
# $9 = Custom event to run


currentUser=`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 + "
");'`
currentUserUID=`id -u "$currentUser"`

ANSWER="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -sender "$4" -title "$5" -message "$6" -closeLabel "$7"  -actions "$8" -timeout 600)"

echo "The user answered: $ANSWER"

case $ANSWER in
    "@TIMEOUT")
        echo "Timeout reached"
        ;;
    "@CONTENTCLICKED")
        echo "You clicked the alert's content !"
        ;;
    "$7")
        echo "Action cancelled"
        ;;
    "$8")
        echo "Action running"
        jamf policy -event $9
        ;;
    **)
        echo "? --> $ANSWER"
        exit 1
        ;;
esac

exit 0

Creating a policy like this:
d8262b10bdf74150b35d75cc3f5065dc

Will result in a notification for the user like this:
9a0549a175fc4f65bb75999bb83d2bed

The custom event mentioned uses this script in my case:

#!/bin/sh

echo "Fullscreen notify"
fsicon="$(mktemp)"
/usr/bin/curl -s -o $fsicon <LINK TO ICON>
fstitle="Installing updates"
fsmessage="Please wait! Do not shut down your computer, this can take up to 10 minutes."
"/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper" -windowType fs -heading "${fstitle}" -description "${fsmessage}" -icon ${fsicon} -lockHUD > /dev/null 2>&1 &

UPDATES=$(/usr/sbin/softwareupdate -l)

RestartRequired=$(echo $UPDATES|grep restart)

if [ "$RestartRequired" ];
        then
                echo "Reboot required"
                echo "Updating"
                /usr/sbin/softwareupdate -ir
                echo "Reconning"
                /usr/local/bin/jamf recon
                echo "Rebooting"
                /usr/local/bin/jamf reboot -background -immediately
                exit 0
        else
                echo "Reboot not required"
                echo "Updating"
                NoRestartUpdates=$(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}')
                /usr/sbin/softwareupdate -i $NoRestartUpdates
                /usr/local/bin/jamf recon
                killall jamfHelper > /dev/null 2>&1
fi

diegogut90
New Contributor III

@kendalljjohnson on the google management account how did you create the pkg for alerter? im having trouble creating that.

kendalljjohnson
Contributor II

@diegogut90 I'm not sure what part your asking about, sorry. Are you referring to the policy that installs the new version of Google or what? Or are you asking about the actual installation of Alerter?

diegogut90
New Contributor III

@kendalljjohnson sorry, the installation of Alerter.

kendalljjohnson
Contributor II

@diegogut90 Oh, gotcha. Grab the zip from the Alerter Github and then use Composer to package it where you want it. I personally dropped it in /Library/Application Support/JAMF/ but the location only really matters for your script to know where to call it from.

kendalljjohnson
Contributor II

Realized I never posted what I finally settled on and put in production in case others want to give it a try. Been running with it for a while and seems to be working pretty well! Thanks for all the input and testing from others in the thread.

#!/bin/bash


# If app is open, alert user with the option to quit the app or defer for later. If user chooses to install it will quit the app, trigger the installation,
# then alert the user the policy is complete with the option to reopen the app. If the app is not open it will trigger the installation without alerting
# Quit and Open path have 2 entries for the times you are quiting/uninstalling an old version of an app that is replaced by a new name (for example quiting Adobe Acrobat Pro, which is replaced by Adobe Acorbat.app)

################################DEFINE VARIABLES################################

# $4 = Title
# $5 = App ID
# $6 = Process Name
# $7 = Jamf Policy Event
# $8 = Quit App Path
# $9 = Open App Path

#Defining the Sender ID as self service due to setting the Sender ID as the actual app being updated would often cause the app to crash
sender="com.jamfsoftware.selfservice.mac"
#Jamf parameters can't be passed into a function, redefining the app path to be used within the funciton
quitPath="$8"
openPath="$9"

################################SETUP FUNCTIONS TO CALL################################

fGetCurrenUser (){
currentUser=`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 + "
");'`

  # Identify the UID of the logged-in user
  currentUserUID=`id -u "$currentUser"`
}

fQuitApp (){
cat > /private/tmp/quit_application.sh <<EOF
#!/bin/bash

/bin/launchctl asuser "$currentUserUID" /usr/bin/osascript -e 'tell application "$quitPath" to quit'
EOF

/bin/chmod +x /private/tmp/quit_application.sh
/bin/launchctl asuser "$currentUserUID" sudo -iu "$currentUser" "/private/tmp/quit_application.sh"
/bin/rm -f "/private/tmp/quit_application.sh"
}

fOpenApp (){
  cat > /private/tmp/open_application.sh <<EOF
#!/bin/bash

/usr/bin/open "$openPath"
EOF

/bin/chmod +x /private/tmp/open_application.sh
/bin/launchctl asuser "$currentUserUID" sudo -iu "$currentUser" "/private/tmp/open_application.sh"
/bin/rm -f "/private/tmp/open_application.sh"
}

################################SETUP TIMER FILE################################

## Set up the software update time if it does not exist already
if [ ! -e /Library/Application Support/JAMF/.$5.timer.txt ]; then
  echo "2" > /Library/Application Support/JAMF/.$5.timer.txt
fi

## Get the timer value
timer=`cat /Library/Application Support/JAMF/.$5.timer.txt`

################################ALERTER MESSAGE OPTIONS################################

saveQuitMSG="must be quit in order to update. Save all data before quitting."
updatedMSG="has been updated. Thank you."

################################START 'UPDATE WITH ALERTER' PROCESS################################

# Look if app is open via process name
appOpen="$(pgrep -ix "$6" | wc -l)"

# if the app is open and the defer timer is not zero
if [[ $appOpen -gt 0 && $timer -gt 0 ]]; then
    fGetCurrenUser
    updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$saveQuitMSG" -closeLabel "Defer ($timer)" -actions "Quit & Update" -timeout 3600)"
    if [[ $updateAnswer == "Quit & Update" ]]; then
        #quit app, install the update, then prompt the user when complete and ask if they want to reopen the app. Message will time out after 60 secs.
        fQuitApp
        /usr/local/bin/jamf policy -event "$7"
        reopenAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$updatedMSG" -closeLabel Ok -actions Reopen -timeout 60)"
        if [[ $reopenAnswer == Reopen ]]; then
            fOpenApp
        fi
        #reset timer after updating
        echo "2" > /Library/Application Support/JAMF/.$5.timer.txt

    else
        let CurrTimer=$timer-1
        echo "User chose to defer"
        echo "$CurrTimer" > /Library/Application Support/JAMF/.$5.timer.txt
        echo "Defer count is now $CurrTimer"
        exit 0
    fi
# if app is open and defer timer has run out
elif [[ $appOpen -gt 0 && $timer == 0 ]]; then
    fGetCurrenUser
    /bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$saveQuitMSG" -actions "Quit & Update" -closeLabel "No Deferrals Left " -timeout 3600
    fQuitApp
    /usr/local/bin/jamf policy -event "$7"
    reopenAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "$4" -sender "$sender" -message "$updatedMSG" -closeLabel Ok -actions Reopen -timeout 60)"
    if [[ $reopenAnswer == Reopen ]]; then
        fOpenApp
    fi
    #reset timer after updating
    echo "2" > /Library/Application Support/JAMF/.$5.timer.txt

else
    # app is not open, reset timer and run updates
    echo "2" > /Library/Application Support/JAMF/.$5.timer.txt
    /usr/local/bin/jamf policy -event "$7"
fi

mottertektura
Contributor

@kendalljjohnson Yes! Thank you for sharing this! I've been using this as well and have even adapted this for OS upgrades and it works great! :)

jaguilar
New Contributor II

@kendalljjohnson Thank you for this. I've been giving your script a try using Google Chrome and it seems to work great, but I'm running into an issue when I click on the "Reopen" button after Chrome is updated. Instead of relaunching Chrome, it is opening up the user's home folder in Finder. Any ideas why it is doing that? I copied your final version of the script and the only change I made was updating the path to the Alerter executable.

jaguilar
New Contributor II

Disregard. I guess I needed to define the Open App Path. Thanks again for the script!

davidhiggs
Contributor III

@kendalljjohnson this is excellent and very well thought out and thanks to those who have shared their mods. I can't believe i missed this! i'll be adding this to my workflow for sure and looking to make my own adjustments. this would be great if jamf could adopt something similar into their product

Kristopher
New Contributor III

Can someone explain how the process is suppose to work for this? I tried setting up a test machine to test this out and all I got was a loop of a patch installing. How do you tell the scrip to popup the notification first? How can you test that? I am a bit confused on the workflow aspect.

There is a policy that triggers the install of the package and then the policy that triggers if the app is open. However... that doesn't seem to be working.

kendalljjohnson
Contributor II

Hey @Kristopher,

There are 2 different policies:

  1. The Alerter policy -This is scoped to a smart group that looks for all computers that have the software installed but not on the desired version -If the user chooses to update and not defer, it calls on the other policy that installs the update via a custom policy trigger
  2. The actual updater policy

Here are an example of the 2 policies (in reverse order).
7b2a51f3a8054a24a8f74b81e126d823

And what script variables I use for Google Chrome.
d5fe155d5e284d5f83897d1dae55cf9c

Hope that helps, happy to answer any questions you might have!

Hi

It’s not working for me. I think I did every step you said, but get an error

 

Script result: /Library/Application Support/JAMF/tmp/Alerter App Updates: line 60: [: /Library/Application: binary operator expected
cat: Support/JAMF/.com.google.chrome.timer.txt: No such file or directory
Checking for policies triggered by "install_googleChromeUpdate" for user "ursare1"...
Executing Policy Install Latest GoogleChrome
Downloading Chrome_96.0.4664.110.pkg...
Downloading https://repository.example.com/Packages/Chrome_96.0.4664.110.pkg...
Installing Chrome_96.0.4664.110.pkg...
Successfully installed Chrome_96.0.4664.110.pkg.
Running Recon...
Retrieving inventory preferences from https://xxxxxx/...
Finding extension attributes...
Locating accounts...
Locating applications...
Locating package receipts...
Searching path: /System/Applications
Locating hard drive information...
Locating software updates...
Locating printers...
Searching path: /Applications
Locating hardware information (macOS 11.6.1)...

 

The only difference I have: my alerter is in /usr/local/bin

But I think, that’s not the point.

Any help?

Thanks 

 

PS I found it: a typo from copy paste. Sorry for that

 

SweetJames
New Contributor II

This is fantastic! Just what I needed for my OS updates. Can you tell me how to get rid of the little tiny terminal icon? e37a985177bf438baffa01eca71a6e91

kendalljjohnson
Contributor II

@bergmire In that example, -appIcon is set as the black/white circle logo and -sender is not set so it provides the generic exec icon. If you were to use -sender "com.jamfsoftware.selfservice.mac" in that same command, the App Icon would remain the same and the exec icon would now be the self service icon. You could not set -appIcon and just use -sender and there would be only 1 icon on the left.

Here are some examples that should help clarify:

alerter -title "Test Alert" -sender "com.jamfsoftware.selfservice.mac" -message "Do something." -closeLabel "Later" -actions "Ok"

8a8e8d1a1e2a461ca5bd26359ffd3f39

alerter -title "Test Alert" -sender "com.jamfsoftware.selfservice.mac" -appIcon "/System/Library/CoreServices/Dock.app/Contents/Resources/dashboard.png" -message "Do something." -closeLabel "Later" -actions "Ok"

0cf2f7a4c4ac4c7d80b915341502bace

alerter -title "Test Alert" -appIcon "/System/Library/CoreServices/Dock.app/Contents/Resources/dashboard.png" -message "Do something." -closeLabel "Later" -actions "Ok"

5f7dabde30464a9c96e6fbd7afb4ec25

Hope that helps!

Captainamerica
Contributor II

Interesting scipts. Can this somehow be changed to prompts that appear central on the page, as those notifications user simply don´t take care about in my company, as so much information appear in these notifications

myronjoffe
Contributor III

@kendalljjohnson Can you use Alerter to open a URL?
I can get it to work in terminal notifier but not Alerter...

kendalljjohnson
Contributor II

@Captainamerica Not that I am aware of. It taps into notification center which is always in the top right corner. For alerts in the middle of the screen I would imaged JamfHelper is your best bet.

@myronjoffe Yes, this is definitely possible. The only trick is getting it to run as the logged in user, not the sudo user sent via Jamf. Here's a quick adaptation I did from an alert I was using. My original was used to alert users that Mojave was available in Self Service and if they wanted to go there the open button would launch Self Service. Here's a to open a website:

#!/bin/bash

################################DEFINE USER################################

currentUser=`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 + "
");'`

  # Identify the UID of the logged-in user
  currentUserUID=`id -u "$currentUser"`


################################ALERTER FUNCTION################################


fAlerter ()
{
updateAnswer="$(/bin/launchctl asuser "$currentUserUID" /Library/Application Support/JAMF/alerter -title "Website" -sender "com.jamfsoftware.selfservice.mac" -message "Check out this website!" -closeLabel "Later" -actions "Open Now")"
if [[ "$updateAnswer" == "Open Now" ]]; then
  sudo su - $currentUser -c "open https://www.jamf.com"
else
    echo "Try again later"
fi
}

fAlerter &

mconners
Valued Contributor

Hello @kendalljjohnson thank you so much for your effort and time to help us out.

I am trying to understand the workflow so please excuse me if this has been hashed out and I simply missed it.

You have a policy that begins the update process and your larger script above will automatically launch the alerter portion if needed? If not needed, your script will begin the process. I see a your main script listed above but also your alerter script. I am trying to figure out if I need both. Thank you for your patience with me.

kendalljjohnson
Contributor II

@mconners

The first policy has a script to check if the app that needs to be updated is running. If it is running, it will prompt for the user to defer or quit the app now to perform the update. If they choose now, it would trigger another policy that actually performs the install. If they defer, it changes the deferral count until the count runs out, so the only option is to update.

If the app is not running, it should call upon the trigger to have the install since the app is not in use.

Hopefully that makes sense, happy to chat through the process if not.

mconners
Valued Contributor

Thank you @kendalljjohnson, so in a sense, the second script is only for using as an alerter for other purposes. The script you gave us really is all self contained, triggering the actual policy to do the install.