Respecting Focus and Meeting Status in Your Mac scripts (aka Don’t Be a Jerk)

In four words Barbara Krueger distills the Golden Rule into an in-your-face admonishment: Don’t be a jerk. It makes for a great coffee mug but how does this relate to shell scripting for a Mac admin and engineer? Well, sometimes a script can only go so far and you need user consent and cooperation to get the job done. Not being a jerk about it can pay off!

A good way to get cooperation from your users is to build upon a foundation of trust and respect. While IT has a long list of to-dos for users: “Did you reboot? Did you run updates? Did you open a ticket? Did you really reboot?” the users might have one for us: “Respect our screens when we’re in the middle of a client meeting and respect when Focus mode is turned on.” And they are right. That’s also two things. “Give an inch and they take a mile” I tell ya!

So how can we in IT can be considerate of our users? First, don’t do anything Nick Burns from SNL does. Second, use the functions below (and in my GitHub) to check if a Zoom, Teams, Webex, or Goto meeting is happening or if Do Not Disturb/Focus mode is on. When non-user-initiated scripts (i.e. daily pop-ups/nags/alerts) run they can bail or wait if the user is busy. If it were real life (and it is!) we wouldn’t walk into a meeting and bug a user, in front of everyone, to run update. If we did, they’d be more likely to remember how rude we were rather than actually running the updates. So let’s get their attention when they will be most receptive to what we have to say.

Detecting Online Meetings Apps

First up is inMeeting_Zoom which simply checks for the CptHost process and returns success or fail. Notice how this simple behavior can be used with an if/then statement. The return code is evaluated by the if, a zero is success and a non-zero is a failure. && is a logical AND and || is a logical OR

#!/bin/sh
#inMeeting_Zoom (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Zoom {
	#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)
	pgrep "CptHost" &>/dev/null && return 0 || return 1
}

if inMeeting_Zoom; then
	echo "In Zoom meeting... don't be a jerk"
else
	echo "Not in Zoom meeting"
fi

Next is another process checker for Webex: inMeetng_Webex. What is a bit more unique is the process appears in ps in parentheses as (WebexAppLauncher)however pgrep cannot find this process (because the actual name has been rewritten by the Meeting Center process). We instead use a combination of ps and grep. A neat trick with grep is to use a [] regex character class to surround a single character, this keeps grep from matching itself in the process list. That way you don’t need to have an extra grep -v grep to clean up the output.

#!/bin/sh
#inMeeting_Webex (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Webex {
	#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)
	ps auxww | grep -q "[(]WebexAppLauncher)" && return 0 || return 1
}

if inMeeting_Webex; then
	echo "In Zoom meeting... don't be a jerk"
else
	echo "Not Webex in meeting"
fi

Goto Meeting is more straightforward, although it should be noted that regardless of quote type, single or double, the parentheses must be escaped with a backslash. Otherwise, it’s the same pattern, look for the process name which only appears during a meeting or during the meeting preview and return 0 or 1 for if to evaluate, find it here: inMeeting_Goto

#!/bin/sh
#inMeeting_Goto (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Goto() {
	#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)
	pgrep "GoTo Helper \(Plugin\)" &>/dev/null && return 0 || return 1
}

if inMeeting_Goto; then
	echo "In Goto meeting... don't be a jerk"
else
	echo "Not in Goto meeting"
fi

Lastly, Teams is a bit more complex, rather than looking for the presence of a process, we instead look for a JSON file in the user’s /Library/Application Support/Microsoft/Teams folder which has the current call status for both the app and the web plugin (the other methods above are for the app only). We’ll use the ljt to extract the value from the JSON. In fact I wrote ljt after starting to write this blog last week and realizing that jpt (weighing in at 64k) was just overkill. As a bonus to doing that, I just realized that bash functions can contain functions! Long ago I ditched using () in shell function declarations and just used the function keyword. Empty parentheses seemed decorative rather than functional since it’s not like it’s a C function that needs parameter names and types. However the lack of parentheses () apparently, prevents a function from being declared inside a function! Below I just wanted to make sure ljt doesn’t get separated from inMeetings_Teams

#!/bin/bash
#inMeeting_Teams (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Teams ()(

	function ljt () ( #v1.0.3
		[ -n "${-//[^x]/}" ] && set +x; read -r -d '' JSCode <<-'EOT'
		try {var query=decodeURIComponent(escape(arguments[0]));var file=decodeURIComponent(escape(arguments[1]));if (query[0]==='/'){ query = query.split('/').slice(1).map(function (f){return "["+JSON.stringify(f)+"]"}).join('')}if(/[^A-Za-z_$\d\.\[\]'"]/.test(query.split('').reverse().join('').replace(/(["'])(.*?)\1(?!\\)/g, ""))){throw new Error("Invalid path: "+ query)};if(query[0]==="$"){query=query.slice(1,query.length)};var data=JSON.parse(readFile(file));var result=eval("(data)"+query)}catch(e){printErr(e);quit()};if(result !==undefined){result!==null&&result.constructor===String?print(result): print(JSON.stringify(result,null,2))}else{printErr("Node not found.")}
		EOT
		queryArg="${1}"; fileArg="${2}";jsc=$(find "/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/" -name 'jsc');[ -z "${jsc}" ] && jsc=$(which jsc);[ -f "${queryArg}" -a -z "${fileArg}" ] && fileArg="${queryArg}" && unset queryArg;if [ -f "${fileArg:=/dev/stdin}" ]; then { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "${fileArg}"; } 1>&3 ; } 2>&1); } 3>&1;else { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "/dev/stdin" <<< "$(cat)"; } 1>&3 ; } 2>&1); } 3>&1; fi;if [ -n "${errOut}" ]; then /bin/echo "$errOut" >&2; return 1; fi
	)

	consoleUser=$(stat -f %Su /dev/console)
	consoleUserHomeFolder=$(dscl . -read /Users/"${consoleUser}" NFSHomeDirectory | awk -F ': ' '{print $2}')
	storageJSON_path="${consoleUserHomeFolder}/Library/Application Support/Microsoft/Teams/storage.json"
	
	#no file, no meeting
	[ ! -f "${storageJSON_path}" ] && return 1

	#get both states
	appState=$(ljt /appStates/states "${storageJSON_path}" | tr , $'\n' | tail -n 1)
	webappState=$(ljt /webAppStates/states "${storageJSON_path}"| tr , $'\n' | tail -n 1)
	
	#determine app state
	if [ "${appState}" = "InCall" ]	|| [ "${webAppState}" = "InCall" ]; then
		return 0
	else
		return 1
	fi
)


if inMeeting_Teams; then
	echo "In Teams Meeting... don't be a jerk"
else
	echo "Not in Teams Meeting"
fi

Detecting Focus (formerly Do Not Disturb)

Last but not least is determining Focus (formerly Do Not Disturb) with doNotDisturb. As you can see there’s been a few different ways this has been implemented over the years. In macOS 10.13-11 the state was stored inside of a plist. For macOS 12 Monterey they’ve switched from a plist to a JSON file. A simple grep though is all that’s needed to find the key name storeAssertionRecords. If it is off, that string is nowhere to be find, when it’s on it’s there. Simple (as in Keep it Simple Stoopid)

#!/bin/bash
#doNotDisturb (grep) (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

#An example of detecting Do Not Disturb (macOS 10.13-12)

function doNotDisturb()(

	OS_major="$(sw_vers -productVersion | cut -d. -f1)"
	consoleUserID="$(stat -f %u /dev/console)"
	consoleUser="$(stat -f %Su /dev/console)"
	
	#get Do Not Disturb status
	if [ "${OS_major}" = "10" ]; then
		#returns c-cstyle boolean 0 (off) or 1 (on)
		dndStatus="$(launchctl asuser ${consoleUserID} sudo -u ${consoleUser} defaults -currentHost read com.apple.notificationcenterui doNotDisturb 2>/dev/null)"

		#eval c-style boolean and return shell style value
		[ "${dndStatus}" = "1" ] && return 0 || return 1
	#this only works for macOS 11 - macOS12 does not affect any of the settings in com.apple.ncprefs
	elif [ "${OS_major}" = "11" ]; then
		#returns "true" or [blank]
		dndStatus="$(/usr/libexec/PlistBuddy -c "print :userPref:enabled" /dev/stdin 2>/dev/null <<< "$(plutil -extract dnd_prefs xml1 -o - /dev/stdin <<< "$(launchctl asuser ${consoleUserID} sudo -u ${consoleUser} defaults export com.apple.ncprefs.plist -)" | xmllint --xpath "string(//data)" - | base64 --decode | plutil -convert xml1 - -o -)")"

		#if we have ANYTHING it is ON (return 0) otherwise fail (return 1)
		[ -n "${dndStatus}" ] && return 0 || return 1
	elif [ "${OS_major}" -ge "12" ]; then
		consoleUserHomeFolder=$(dscl . -read /Users/"${consoleUser}" NFSHomeDirectory | awk -F ': ' '{print $2}')
		file_assertions="${consoleUserHomeFolder}/Library/DoNotDisturb/DB/Assertions.json"

		#if Assertions.json file does NOT exist, then DnD is OFF
		[ ! -f "${file_assertions}" ] && return 1

		#simply check for storeAssertionRecords existence, usually found at: /data/0/storeAssertionRecords (and only exists when ON)
		! grep -q "storeAssertionRecords" "${file_assertions}" 2>/dev/null && return 1 || return 0
	fi
)
if doNotDisturb; then
	echo "DnD/Focus is ON... don't be a jerk"
else
	echo "DnD/Focus is OFF"
fi

Since Focus can remain on indefinitely an end user may never see your pop-up. If so, build a counter with a local plist to record and increment the number of attempts. After a threshold has been reached you can then break through to the user (I certainly do).

Detecting Apps in Presentation Mode

A newer addition to this page is some code to detect fullscreen presentation apps that I cannot take credit for. Adam Codega, one of the contributors to Installomator hipped me to a cool line of code that was added in PR 268. It leverages pmset to see what assertions have been made to the power management subsystem. It uses awk to look for the IOPMAssertionTypes named NoDisplaySleepAssertion and PreventUserIdleDisplaySleep with some additional logic to throw out false positives from coreaudiod. In testing I’ve found this able to detect the presentation modes of Keynote, Powerpoint and Google Slides in Slideshow mode in Chrome (but not Safari), your mileage may vary for other apps. Another caveat is that when a YouTube video is playing in a visible tab, it will assert a NoDisplaySleepAssertion, however these will be named “Video Wake Lock” whereas a Slideshow presentation mode will have its name assertions named “Blink Wake Lock”. So I am adding an additional check to throw our “Video Wake Locks”. This may be more of a can of worms than you’d like, if so, user education to set Focus mode may be the way to go. A functionalized version can be found here: inPresentationMode

#!/bin/sh
#inPresentationMode (20220319) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#with code from Installomator (PR 268) (https://github.com/Installomator/Installomator) Copyright 2020 Armin Briegel
#Licensed under the MIT License

function inPresentationMode {
	#Apple Dev Docs: https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes
	#ignore assertions without the process in parentheses, any coreaudiod procs, and "Video Wake Lock" is just Chrome playing a Youtube vid in the foreground
	assertingApps=$(/usr/bin/pmset -g assertions | /usr/bin/awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ && ! /Video\ Wake\ Lock/ {gsub(/^.*\(/,"",$0); gsub(/\).*$/,"",$0); print};')
	[ -n "${assertingApps}" ] && return 0 || return 1
}

if inPresentationMode; then
	echo "In presentation mode... don't be a jerk"
else
	echo "Not in presentation mode..."
fi

All together now

Putting it all together here’s how you can test multiple functions in a single if statement, just chain them together with a bunch of || ORs

#!/bin/bash
#Joel Bruner - demo of meeting/focus aware functions for your script

#pretend we've declared all the functions above and copy and pasted them in here
function doNotDisturb()(:)
function inMeeting_Teams()(:)
function inMeeting_Zoom(){:}
function inMeeting_Goto(){:}
function inMeeting_Webex(){:}
function inPresentationMode(){:}

#test each one with || OR conditionals
#the FIRST successful test will "short-circuit" and no more functions will be run
if doNotDisturb || inPresentationMode || inZoomMeeting || inMeeting_Goto || inMeeting_Webex || inMeeting_Teams; then
	echo "In a meeting, presentation, or Focus is On... don't be a jerk"
	#do something else, like wait 
else
	echo "Not in a meeting..."
	#alert the user or do whatever you needed to do that might impact them
fi

Using these functions in your scripts can help respect your users’ online meeting times and Focus states. Also it doesn’t hurt to document it somewhere and toot your own horn in a user facing KB or wiki. If and when a user complains about that pop-up that destroyed their concentration and their world, you can show them the forethought and effort you’ve taken to be as considerate as possible regarding this perceived incursion. This usually has the effect of blowing their mind 🀯 that someone in IT is actually trying to be considerate!

P.S. I’m pretty stoked that Prism.js can really jazz up my normally dreary grey code blocks! πŸ˜πŸ€“ I found a good WordPress tutorial here