I run Linux,…or does it run me? Some computing paradigms are so ubiquitous, so ingrained, we rarely stop to think that things could work another way. When such a realization comes, we can exercise our freedom – one of Fedora’s four foundations – to improve the user experience. For that sentiment to be more than cliché, I needed to re-imagine the idea of the default printer and how it gets set. This article presents that implementation.
Motivation
My printing needs are simple. I’ll occasionally print a few sheets of music from lilypond, a coupon from a web site, or a page of account information that I’m supposed to retain on file for tax purposes. Selecting a printer only figures into it when I install an operating system from scratch, or when I replace my printer. Otherwise, I’m using my default – and only – printer.
My son’s printing needs are quite different. He brought home a specialty label printer that a neighbor had put out on the curb as trash. The challenge of rehabilitating this esoteric device, and getting it to work with Linux, was irresistible. And he pulled it off! That printer was soon joined by another, then another. Used label printers, parts, and large rolls of label stock from eBay started arriving. Soon he was taking orders from paying customers for novelty and custom labels, all driven by CUPS with free fonts, GIMP, Inkscape, and home-grown scripts.
Specialty label printer control includes a raft of functions most of us “plain paper” users never deal with: knife presets, temperature settings, stock rewinding between color passes, etc. Any project will require multiple setup and test print jobs before the final printing. With about a dozen different printers to choose from, each with its own unique combination of capabilities, one thing is almost always true: the default printer is not the one he needs for the next project.
Command line printing programs generally expect to be told what printer to use if not the default. Most graphical applications present a list of printers when you choose to print, with the default printer pre-selected. Switching between multiple command line and graphical printing applications without first changing the designated default printer leads to a mistake eventually – either by picking the wrong printer or neglecting to pick one at all.
It’s worth pointing out that changing your default printer is just not that hard. “Set as default” is a click away in the Print Settings dialog. But it’s also one more thing to do, a thing that is surprisingly easy to put off or overlook entirely. Apparently, once you’ve started printing to a particular printer, you’ve mentally changed your default printer even if you haven’t changed your computer’s default printer. Discussing this one day, my son said, “It would be helpful if, whenever I print, the system would make whatever printer I just used be the default printer.” I was persuaded, but there is no such check box on the system.
Determining the last used printer
Although most users interact with CUPS (Common Unix Printing System) through applications, it also sports a robust command line interface. The lpstat command lists the status of printers or print jobs. Adding the parameters -W all will show all print jobs for the current user regardless of whether they’ve completed. A -u with no user name after it will do the same for all users’ print jobs. Conveniently for our purposes, lpstat -W all -u lists jobs in chronological order. The last line is the only one of interest to us. The first field on each line is the job name. This consists of the printer name (the part you want) followed by a hyphen and the job number.
$ lpstat -W all -u HP-LaserJet-4250-5 utoddl 393216 Tue 21 Nov 2017 09:26:09 PM EST HP-LaserJet-4250-6 root 393216 Tue 21 Nov 2017 09:27:07 PM EST HP-Deskjet-952c-Printer-7 utoddl 171008 Sat 25 Nov 2017 05:00:42 PM EST [...] HP-LaserJet-4250-51 utoddl 132096 Wed 23 Jun 2021 08:43:53 AM EDT HP-Deskjet-952c-Printer-52 root 28457 Sun 27 Jun 2021 11:24:50 AM EDT
A bit of bash scripting can isolate the name of the last used printer from the hyphen and number in the job name. The lpadmin command can set the default printer:
while read -a line; do printer=${line[0]%-*} done < <(lpstat -W all -u) lpadmin -d $printer
Detecting and acting on print jobs
Now that you have bash code to set the default printer, you need to know when to run it. You could put it in an infinite loop, sleeping for 30 seconds between default printer resets. Or you could monitor the system logs for print activity and act accordingly. Those are terribly wasteful approaches. A better way is to have the system notify you when the /var/spool/cups directory changes, which it does with every print job. Modern Linux kernels offer that functionality through the inotify interface. User space programs can use the inotify interface to ask the Linux kernel to notify them upon certain changes to specific files or directories.
Numerous utilities available on the internet facilitate accessing the inotify interface from scripts. Fortunately, you don’t need any of them. Fedora already has a mechanism for activating services through inotify: systemd.
Systemd
Systemd manages services based on system events and conditions. All of these are defined in systemd unit files. Besides simply starting and stopping services when the system boots or shuts down, systemd also pairs certain event units with similarly named service units. For example, the logrotate.timer unit defines a timer which, when it goes off, activates the logrotate.service.
Path unit
A path unit uses the kernel’s inotify interface to watch for changes in specified files or directories. You will need to create a path unit file named default-printer-update.path which will trigger when the /var/spool/cups directory changes. You’ll also create a corresponding service unit file named default-printer-update.service that contains the bash code to set the default printer. That service unit will activate whenever the path unit triggers. I chose the name to fit with the conventions of other units in Fedora — what it’s acting on followed by what action it’s taking, in all lower-case ASCII letters with separating hyphens. Both the path and service unit files go in the /etc/systemd/system directory. This is owned by and only writable by the root user. Thus, you’ll need to use sudo to get elevated privileges and your favorite text editor to create and edit these files. Here’s the path unit file. (See man systemd.path.)
# /etc/systemd/system/default-printer-update.path [Unit] Description=Default printer update [Path] PathChanged=/var/spool/cups [Install] WantedBy=multi-user.target
Service unit
The service unit file follows. See man systemd.service for definitive explanations, but I’ll point out a few things. Type=oneshot means this unit simply runs a command; it doesn’t create any background processes that need to be monitored and shutdown later. The command in the second ExecStart line is more complicated than our bash code above, while the alternative in the first commented out ExecStart line is a bit simpler. Either will work. The longer second one attempts to set the default printer only when it differs from the current default printer, and it always outputs some text that will show up in the system’s journal to help make debugging easier.
Together these two examples show how you must handle certain characters when you include non-trivial commands in Exec lines of service unit files. Backslashes and quotes have to be escaped with a backslash, dollar signs have to be doubled, and new-lines are lost altogether so separating semicolons must be inserted. Though tricky and perhaps interesting to some, further details are beyond the scope of this article.
# /etc/systemd/system/default-printer-update.service [Unit] Description=Default printer update [Service] Type=oneshot # ExecStart=/bin/bash -c 'lpadmin -d $( lpstat -W all -u | tail -n 1 | cut -f1 -d\\ | sed -E -e 's/-[0-9]+$//' )' ExecStart=/bin/bash -c 'lpstat_d=$$( lpstat -d ) ;\ default_printer=$${lpstat_d##* } ;\ last_job_id=$$( lpstat -W all -u | tail -n 1 | cut -f1 -d" " ) ;\ last_printer=$${last_job_id%%-*} ;\ if [ \"$$last_printer\" != \"$$default_printer\" ] ; then \ echo \"Updating default printer to $$last_printer.\" ;\ lpadmin -d \"$$last_printer\" ;\ else \ echo \"Leaving default printer as $$default_printer.\" ;\ fi' [Install] WantedBy=multi-user.target
Reloading systemd
Once you have the two unit files in /etc/systemd/system (with mode 0644, owner root, and group root set) you have to use the following command for the changes to take effect:
sudo systemctl daemon-reload
Any time you make changes to either of the unit files, you must perform this reloading step.
To start testing, you need to enable and start the path unit. Enabling the service unit isn’t necessary; it will start when the path unit triggers.
sudo systemctl enable default-printer-update.path
sudo systemctl start default-printer-update.path
Finally you’re ready to test.
Testing
Let me point out two things that will seem obvious once stated but that may not occur to someone just setting this up. First, this will not work on a system where no-one has ever printed, or where the last used printer is no longer defined, or where the print job history has been flushed. Fortunately it won’t hurt anything, either. Second, you don’t actually have to print anything to test this out. It’s sufficient to sudo touch /var/spool/cups to trigger the path unit which then activates the service. This can save a lot of time, paper, and what’s really expensive: printer ink.
Setup
You’ll need at least two printers defined. These don’t have to physically exist or to be attached to your machine. They can both point to the same physical printer as long as they are defined with different names. If you need to, go ahead and fire up system-config-printer and define whatever printers you want.
These commands can be useful as you deal with issues while testing. The command lpstat -a will list all defined printers, lpstat -d will show the current default printer, and sudo lpadmin -d printer_name will set the default printer.
Process
Open two terminals, and in one of them enter the command journalctl -f. This will show the system journal messages as they occur, including messages from systemd about activating the default-printer-update.service and those coming from the default-printer-update.service itself.
In the other terminal, enter sudo touch /var/spool/cups, and you should see messages in the first terminal like this:
Oct 02 15:12:04 lappy systemd[1]: Starting Default printer update… Oct 02 15:12:04 lappy bash[35263]: Leaving default printer as Generic-CUPS-BRF-Printer0. Oct 02 15:12:04 lappy systemd[1]: default-printer-update.service: Deactivated successfully. Oct 02 15:12:04 lappy systemd[1]: Finished Default printer update.
That these messages showed up at all indicates that the path unit is working and activated the service unit. In this case the default printer was already set properly. Otherwise the message would have said, “Updating default printer to <some_printer_name>.”
Our work here is done!
Selinux
At this point I had expected to have to explain how to persuade selinux to allow our path unit to watch the /var/spool/cups directory. That was an issue when my son and I first set this up over a year ago. I was pleasantly surprised to find, as man init_selinux shows, that processes of type init_t (like systemd itself on behalf of your path unit) can now manage files and directories of type print_spool_t (like /var/spool/cups).
That selinux enhancement is typical of the thousands of often unnoticed improvements tucked away into esoteric corners of Fedora and other distributions that make using Linux a viable option in a rapidly changing technological landscape. It’s built on a lot of work from a lot of people, and a lot of fun. I’m grateful to them that I get to work on and share little projects like this one. I hope you enjoyed it.
Darvond
So wasn’t there a CUPS replacement in the works since Apple had clearly abandoned it?
Whatever became of that?
Eduard
CUPS is now under develop by OpenPrinting, and that’s the version shipped in Fedora:
sudo dnf info cups
Last metadata expiration check: 0:35:47 ago on Fri 15 Oct 2021 01:19:42 PM -03.
Installed Packages
Name : cups
Epoch : 1
Version : 2.3.3op2
Release : 7.fc34
Architecture : x86_64
Size : 7.6 M
Source : cups-2.3.3op2-7.fc34.src.rpm
Repository : @System
From repo : updates
Summary : CUPS printing system
URL : https://openprinting.github.io/cups/
License : ASL 2.0
Description : CUPS printing system provides a portable printing layer for
: UNIX® operating systems. It has been developed by Apple Inc.
: to promote a standard printing solution for all UNIX vendors and users.
: CUPS provides the System V and Berkeley command-line interfaces.
Richard England
Are you thinking of this, perhaps? https://www.msweet.org/pappl/
I believe Michael Sweet has forked CUPS and is supporting “his” version.
https://www.msweet.org/
Darvond
It’s entirely possible. Or the OpenPrinting project.
laolux
Great article! I don’t think (and hope) that my printing needs will ever be that complicated, but it was fun to read nonetheless. Always interesting to see what problems and solutions others come up with.
David
This sounds great, but one question: Could that complex Exec line in default-printer-update.service be replaced by a shell script doing those things? Thanks,