Running a script when a user prints
Some organizations (particularly schools and universities) require users to go to a web page to authenticate print jobs before those jobs can actually be printed. However, there is no documented way of automatically directing a user to a web page after they have printed. This article will demonstrate one way of doing this.
Warnings
This setup is a huge hack, and it is very likely that any system update that involves printers would overwrite it (requiring the hack to be re-installed). So this solution will best work on groups of computers that a system admin has complete control over. That said it also seems to currently be the best way of having a script run when something is printed.
Overview
Unfortunately the version of the printing system that comes with MacOS 10.5 (CUPS) does not have any obvious places to plug in to to get the behavior that we want, but there are a few places where we can hack into it to get the behavior we want. CUPS has a series of filters that print jobs get sent through to apply various transformations to the job to convert it to a format that the printer can understand. By causing the print job to take a little detour along this chain of filters we can capture the information we want, and still send the job on its way. The only filter that we can be sure that every print job goes through is the pstops
(Postscript-to-Postscript) filter. In fact the CUPS system uses this filter to do a number of things, including print accounting and selecting page ranges.
But intercepting the job is just one part of what we need to do. Because the print filters run as root, we have to hand off a message to another process running as the user to act upon. Using a launchd item and a watched folder makes this process relatively easy. Our replacement filter will create empty files specific directory as a signal, and the launchd item will start a user-level process that will do most of the work and then erase the signal file.
Replacing the filter
To intercept the print job we have to move aside the pstops
filter, and put our own filter in its place. But since we still want the functionality that pstops
provides, we will call the normal pstops
filter from within our replacement. The steps to do all this:
Open Terminal.app and the go to the filters folder with:
cd /usr/libexec/cups/filter
Change the name of the original
pstops
filter topstops.binary
withsudo mv pstops pstops.binary
Note that this will ask you for your password (you have to be logged in as an admin)
Create our replacement item. You can do this a number of methods, to do it with pico in the teminal:
sudo pico pstops
copy-and-paste the following:
#!/bin/bash WATCHED_FOLDER='/var/db/runAtPrint' # place the signal file, and make sure it can be deleted by the user-level process # note: $PRINTER is a environmental variable set by CUPS /usr/bin/touch "$PRINTER_FOLDER/$PRINTER" /bin/chmod 0666 "$PRINTER_FOLDER/$PRINTER" # run the original pstops binary with the same input we were given exec $0.binary "$@"
Then use <control>-x to exit pico, answer "y" when it asks to save, and use
to accept the default name. Give our replacment filter the proper permissions with:
sudo chmod 0555 pstops
The owner and group should already be correct (root:wheel)
Creating the signal folder
The folder that we will use to transfer signals betwen the filter and the user-level process needs to exist before either side of our our process will work, so we create it ahead of time:
Create the folder with:
sudo mkdir /var/db/runAtPrint
If you want to choose a differnt folder remember to change it in the
pstops
wrapper script, and in the launchd item in the next section.Make sure that it has the proper permissions so that both the filter and the user-level process can create/destroy itmes here with:
sudo chmod 0777 /var/db/runAtPrint
Create the user-level script
The user-level script is what will pick up the signal files left by the filter, make sure that they are for printers on the proper server, and then call the user's choice of web browser. This part of the solution should probobably be cusomized to the indvidual institution, so this script is more of a reference implimentation. We are going to place the script into /Library/Scripts
but in practice this script should go into the same place as any other management scripts your setup might have (the author usually create a folder at /Library/Management
for this sort of stuff and a "Scripts" folder in that).
This reference script will ignore print jobs that go to printers that are not lpd queues on the server "printing.macenterprise-example.com" (a domain that does not exist), so at a bare minimum you will need to change the lines starting with "printingServer =" and "printingPage =" to reflect your environment. Any real use of this system will probably require at least a little more cusomization than that.
Use pico in Terminal.app to create the script file:
sudo pico /Library/Scripts/runAtPrint.py
copy-and-paste the following:
#!/usr/bin/env python import re, os, time ''' This watches the printer folder, and whenever there is an appropriate item there launches a browser at the right page ''' printerFolder = '/var/db/runAtPrint' printingServer = 'printing.macenterprise-example.com' printingPage = 'https://printing.macenterprise-example.com/printer.php?printer=' busyTimeout = 10 # wait no more than this number of seconds lpstatRegex = re.compile('^device for (?P<printerName>\S+): lpd://(?P<server>\S+?)/(?P<queueName>\S+)') printerBusyRegex = re.compile('^printer \S+ now printing') # setup a list of printers that we will watch watchedPrinters = {} for thisPrinter in os.popen('/usr/bin/lpstat -s'): result = lpstatRegex.search(thisPrinter) if result and result.group("server") == printingServer: watchedPrinters[result.group("printerName")] = result.group("queueName") # process the files in the watched directory for thisPrinter in os.listdir(printerFolder): if thisPrinter in watchedPrinters: printerIsBusy = True timeout = time.time() + busyTimeout # wait until the print server is done spooling while printerBusyRegex.search( os.popen('/usr/bin/lpstat -p ' + thisPrinter).read() ) and time.time() < timeout: time.sleep(.25) # open a new window in the user's browser to the right page os.system('/usr/bin/open %s%s' % (printingPage, watchedPrinters[thisPrinter])) # remove the file even if it was not a printer we mange os.unlink( os.path.join(printerFolder, thisPrinter) ) os._exit(0)
Then use <control>-x to exit pico, answer "y" when it asks to save, and use
to accept the default name. Make sure that the script has the proper permissions:
sudo chmod 0755 /Library/Scripts/runAtPrint.py
Create the launchd item
We want the user-level script to fire off every time our filter places something in the /var/db/runAtPrint
folder, and Apple's Launchd system provides a nice facility to do exactly that. But we need to give it a plist to set this up. There are a number of ways of doing this, and ususally the author would use Property List Editor.app to create the file, but for simplicitie's sake we will continue to use pico to do so:
Use pico in Terminal.app to create the launchd plist:
sudo pico /Library/LaunchAgents/org.macenterprise.runAtPrint.plist
copy-and-paste the following:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.macenterprise.runAtPrint</string> <key>Program</key> <string>/Library/Scripts/runAtPrint.py</string> <key>RunAtLoad</key> <false/> <key>QueueDirectories</key> <array> <string>/var/db/runAtPrint</string> </array> </dict> </plist>
Then use <control>-x to exit pico, answer "y" when it asks to save, and use
to accept the default name. Note: if you decide to change the name of this file, make sure that you keep the "Label" item in the plist in sync with the name. Launchd can get a little difficult to work with if the names to not match.
Make sure that the launchd item has the proper permissions:
sudo chmod 0644 /Library/LaunchAgents/org.macenterprise.runAtPrint.plist
It should already have the proper group and owner (root:wheel).
Final Notes
At this point you should be able to log out, then back in, and if you did everything correctly the system should be running. As was mentioned in the Caution section, any Apple system update that changes printing has a chance of replacing the pstops
file. In that case you would just need to move the new version to replace the old one you moved asside, and put the replacement script in its place again.
In case you missed it in that section: the user-level script does need to be customized to the environment that it will be running in. The reference script should be a good starting point for that customization.
No comments:
Post a Comment