Configuring Printers Programmatically for AirPrint

For the past several weeks, I’ve been troubleshooting an issue one of our users reported when printing large MB files to an HP Color LaserJet Pro M452.

Historically, we’d had trouble getting these M452s to print jobs sent from Macs routed via our print server and temporarily configured direct IP printing to work around it. However, after upgrading to macOS Mojave (10.14.6) this year and temporarily implementing a solution proposed on Jamfnation to get things working with our print server, we ran into a different set of issues entirely.

While test pages and files ~5MB in size would print just fine, files closer to ~10MB would take an exceptionally long time to process – getting stuck spooling at several points – and when ultimately finished after several minutes only printed out the following error:

ERROR: limit check

Interestingly, the print experience was the same regardless if we were going through our print server, via direct IP, and even via a physical USB connection.

Given our previous issues with print server printing, I tried the following for direct IP printing (lpd://), all with no success:

  • Using both the available driver from HP’s website and Generic Postscript Printer driver
  • Testing different file types totaling ~10MB, including saving several large test images as PDFs before printing
  • Manually adding the printer via the ‘Add Printer’ menu as well as programmatically
  • Booting the test Macs to Safe Mode
  • Testing with different Macs running 10.14.6
  • Attempting to upgrade the firmware of the printer itself using the HP firmware update utility failed to transfer
  • Upgrading the Macs running 10.14.6 to the latest available supplemental update

The one thing that did successfully print ~10MB files without issue were Macs running 10.13.6. So, something is definitely not happy in Mojave …

We went so far as to purchase a new HP M454, but unfortunately ran into the same issues.

Desperately hoping for a long-term solution, I reached out to Apple Enterprise support, and after being escalated was able to confirm with the advisor that the printer error we were seeing is a known issue. Besides confirming that most issues reported were with HP printers, it has also been identified both on 10.14 and 10.15 …

The only good news here was that there is a known workaround available: configure printing via AirPrint. Having handled all our printing configurations with the help of PrinterGenerator, I needed to figure out how we could programmatically setup AirPrint our few M452s and new M454s.

With some help with members of the #printers-n-cups MacAdmin Slack channel, I learned that AirPrint utilizes the ipp protocol. When manually configuring a printer for AirPrint, a built-in macOS tool is used called ipp2ppd (/System/Library/Printers/Libraries/ipp2ppd) and which creates a ppd file in /etc/cups/ppd.

Running ipp2ppd with no arguments reveals it’s usage:

Usage: ipp2ppd <printer_uri> <input_ppd_path>

With the printer installed for AirPrint manually, you can get the settings with lpoptions, critically the device-uri, which we need for <printer_uri>:

# To get printer names
lpstat -e
# Get printer settings
lpoptions -p <printer_name>

In our case, we’re working with ipp:// rather than dnssd://. With the printer uri, the last piece is supplying a ppd path. A little searching on Jamfnation revealed the buried built-in AirPrint.ppd file (although you can just supply everywhere and get the same result):


Now, running the ipp2ppd tool with the required arguments produced the necessary ppd information! Albeit, to stdout. Redirecting to a file produces the needed ppd file.

/System/Library/Printers/Libraries/ipp2ppd ipp://<printer_ip> /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/Resources/AirPrint.ppd > /path/to/<printer_name>.ppd

Because this file normally gets created in /etc/cups/ppd, you can simply redirect the output to a file there. In my tests, this must be done as root.

With the AirPrint ppd file created, you’ll want to test using it to install the printer:

lpadmin -p <printer_name> -D "<printer_display_name" -L "<printer_location>" -E -v ipp://<printer_ip> -P /path/to/<printer_name>.ppd

With the printer installed programmatically, we can test printing our large image file and verify that a ~10MB file now successfully prints!

If you happen to use PrinterGenerator or a similar munki nopkg item to install your printers via postinstall_script, I created the preinstall_script below to create the necessary AirPrint ppd file via ipp2ppd:

# Use the built-in ipp2ppd tool to create a PPD file for AirPrint
# Create the PPD file
$EXE ipp://${PRINTER_IP} "$AIR_PPD" > "$PPD"
view raw hosted with ❤ by GitHub

A Small Caveat …

The one caveat I found is that running this script as a local user produced a slightly different ppd file than when run as root.

Mysteriously, the ppd has a single line that is different when run as root: it did not have a printer icon listed. Run as a local user, the printer’s icon file is copied to /Library/Printers/Icons and is then referenced in the ppd file.

This missing icon behavior occurred both when adding sudo as well as when attempting to run by root (running sudo -s) as a local user (ex. as root: sudo -u <username> ipp2ppd ...). With the help of a fellow Mac Admin in the MacAdmins #printers-n-cups channel, we found that a local user could successfully get the icon when run as root by adding the -E flag:

sudo -E -u <user> ipp2ppd ipp://<printer_ip> /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/Resources/AirPrint.ppd > /path/to/<printer_name>.ppd

Unfortunately, this doesn’t really help us because when run via munki, Jamf, or some other method it will still run as root. So as of yet, I’ve been unable to programmatically configure AirPrint printers which also include the printer icon.

With a little help from Wireshark, I was able to determine that as part of its printer attributes query (you can run this manually by running ipptool -tv ipp://<printer_ip>/ipp/print get-printer-attributes.test) ipp2ppd uses the printer-icons information collected to download 3 files (in the case of the M452):


Once downloaded, these are converted & merged into a single .icns file which is populated in /Library/Printers/Icons. The traffic shows that these .png files are also collected when ipp2ppd is run as root, but does not produce this .icns file and likely why it does not then subsequently include this in the final ppd file.

Despite not being terribly important on the whole, this does itch my curiosity and my hope for an eventual fix.

Addressing Users Who Don’t Logout on Lab & Cart Macs

One of the challenges in multi-user Mac environments is what results when a user fails to logout of their account. Good security practice says that after a time of user inactivity to trigger the lock screen. But if multi-user switching isn’t enabled, then the machine is effectively locked out for other users.

If you do opt to enable multi-user switching then overall machine performance is degraded with multiple user sessions running simultaneously. To make matters worse, attempting to restart or shutdown gracefully with more than one active user account requires admin credentials. And even if you happen to configure an automated reboot schedule for your machines, if a user has an unsaved document open the Mac’s save dialog prompt will stall this. SO frustrating!

While I had explored some open-source tools in the past to try and address this, I didn’t like the idea of messing with the sudoers file to allow standard users to restart and shutdown. I had tried to come up with my own script to determine if multiple users were logged in and kill all of those sessions, but I couldn’t figure out a clean way to accomplish this without inadvertently killing the active user’s session.

Ultimately, with a helpful recommendation on Jamfnation, I reasoned that it was better to fully restart a multi-user Mac than try to work out who to forcibly logout. I came up with the script below to handle this:

# Determine loginwindow processes running and reboot the machine if
# more than 1 is detected.
USER=$(/usr/sbin/scutil <<< "show State:/Users/ConsoleUser" | /usr/bin/awk -F': ' '/[[:space:]]+Name[[:space:]]:/ { if ( $2 != "loginwindow" ) { print $2 }}')
# Searches for loginwindow processes and calculates the number running
LOGIN_PROCESSES=$(/usr/bin/pgrep loginwindow)
COUNTLOGINS=$(/bin/echo "$LOGIN_PROCESSES" | /usr/bin/wc -l | /usr/bin/sed 's/ //g')
writelog () {
/bin/echo "${1}"
/bin/echo $(/bin/date) "${1}" >> $LOG
# If number of loginwindow process is greater than 1, runs the script
if [ "$COUNTLOGINS" -gt "1" ]; then
writelog "Multiple logins detected. Running script …"
writelog "Only 1 user detected. Exiting."
exit 0
for ((i = 0; i < ${#LOGIN_PROCESSES[@]}; i++)); do
# Get the loginwindow process owner given the pid and write to array
P_OWNER+=($(/bin/ps -eo user,pid | /usr/bin/grep -w "${LOGIN_PROCESSES[$i]}" | /usr/bin/awk '{print $1}'))
for ((i = 0; i < ${#LOGIN_PROCESSES[@]}; i++)); do
# If process owner is not the current active user OR root, reboot the machine
if [ "${P_OWNER[$i]}" != "$USER" ] && [ "${P_OWNER[$i]}" != "root" ]; then
# Force reboot machine
writelog "Rebooting to clear logged in user ${P_OWNER[$i]}"
/sbin/shutdown -r now
view raw hosted with ❤ by GitHub

With this determined, the question became how to trigger the script. While offset is for logouts what outset is for logins, I didn’t intend to implement another tool just for this one purpose. I also didn’t want to have this run periodically and potentially interfere with a logged in user.

I reasoned (however wistful it may be) if a user was logging in to a machine that already had a user logged in they would be more likely to logout, and at that time it would be the least intrusive time to trigger a reboot since the active user had finished using it. Since Jamf can trigger policies at logout and also cache policies for running offline, this seemed like the best option.

Putting this additional policy in place along with our existing restart schedule, I would say that we are at least a little less likely to have extended multi-user login issues.