Home Blog

The print queue that reported success and printed nothing

There is a Canon imageRUNNER ADVANCE C5550 on my floor. I can reach its web interface, so adding it to my Arch box should have been five minutes. It took rather longer, and the interesting part is not the fix — it's that CUPS spent the whole time telling me the job had printed.

Finding out what it speaks

Discovery first. The copier answers ping, and a quick sweep of the usual ports says a lot:

for p in 631 9100 515 80 443; do
  timeout 2 bash -c "echo > /dev/tcp/10.0.0.50/$p" 2>/dev/null \
    && echo "$p OPEN" || echo "$p closed"
done

Port 631 is closed. No IPP means no driverless setup, no IPP Everywhere, and nothing for avahi to find — which also explains why the desktop print dialog had never noticed this machine. Port 9100 is open, so raw it is.

Before picking a driver, ask the device what it can parse. SNMP knows:

snmpget  -v1 -c public 10.0.0.50 1.3.6.1.2.1.1.1.0
snmpwalk -v1 -c public 10.0.0.50 1.3.6.1.2.1.43.15.1.1.2

The first returns Canon iR-ADV C5550 /P. The second returns a column of bare integers, which decode through the PrtInterpreterLangFamilyTC enum: 3 is PCL, 6 PostScript, 47 PCL-XL, and 54 PDF.

PDF is the useful one. If the copier renders PDF natively, CUPS can hand it PDF and skip rasterising entirely, which means the Generic PDF PPD that ships with cups-filters is all the driver I need — no proprietary Canon package:

sudo lpadmin -p iR-ADV-C5550 \
  -v socket://10.0.0.50:9100 \
  -P /usr/share/ppd/cupsfilters/Generic-PDF_Printer-PDF.ppd \
  -o PageSize=Letter -o Duplex=DuplexNoTumble -o ColorModel=color -E

Queue created, idle, no errors. I sent a test page. CUPS moved it to the completed list. No paper.

Completed means transmitted

This is the part worth keeping.

A socket:// queue speaks raw JetDirect to port 9100. It opens a TCP connection, writes the job, and closes it. There is no protocol on top — no acknowledgement, no status, no job ID coming back. So the backend has exactly one fact available to it: the bytes went into the socket without an error. It reports success on that basis, because that is all it can ever know.

"Completed" in a raw queue does not mean printed. It means transmitted. A copier that accepts every byte and then drops the job on the floor is indistinguishable, from the host's side, from one that printed it perfectly.

Which is precisely what was happening. The device's own job log:

5015  Error  09/14/2026 10:56:03 AM  Printer Print  <user>  0  0  0 x 0  #701

Canon's #701 is "the Department ID does not exist, or the PIN is incorrect". The copier had Department ID Management enabled — the accounting feature that makes a shared machine demand an ID and PIN before it will do anything. My job arrived with no credentials, so it was logged and binned. Zero pages.

CUPS could not have told me this. Nothing was wrong on my end of the wire.

The credentials go in the job

Canon's Windows and macOS drivers have a Department ID field. On Linux with a generic PPD there is no such field, so the credentials have to be smuggled into the job stream itself, as PJL in the header.

Canon accepts them through a proprietary extension:

@PJL COMMENT CANPJL SET USERID = 1234567
@PJL COMMENT CANPJL SET USERPWD = 1234

USERID is the Department ID and USERPWD is the PIN. I want to be straight about the provenance here: Canon's published PJL reference does not document these commands at all. I found them in a support-forum post from someone doing the same thing to an imageRUNNER 4570, and I had no way to confirm them except by trying.

Which raises the obvious problem — if a failed job is silent, how do you know a successful one worked?

Trusting the counter, not the queue

Every printer maintains a lifetime page count, and SNMP will hand it over:

snmpget -v1 -c public -Ovq 10.0.0.50 1.3.6.1.2.1.43.10.2.1.4.1.1

That number is generated by the device, not by my host, and it only moves when paper physically moves. It is the one signal in this entire setup that cannot be faked by a well-behaved TCP write.

So: read the counter, send one job with the PJL header wrapped around a PDF payload, read it again.

<ESC>%-12345X@PJL JOB NAME="cups-test"
@PJL COMMENT CANPJL SET USERID = 1234567
@PJL COMMENT CANPJL SET USERPWD = 1234
@PJL ENTER LANGUAGE = PDF
...pdf bytes...
<ESC>%-12345X@PJL EOJ
<ESC>%-12345X

The counter went from 455686 to 455687. Printer status went printing(4) and back to idle(3), with hrPrinterDetectedErrorState staying 00 00 throughout. One sheet, as requested. The undocumented commands are real.

Making it permanent

A one-off script is not a print queue. To get this into normal lp and the GTK print dialog, the header has to be prepended to every job, which means a CUPS backend.

The backend is a shell script at /usr/lib/cups/backend/canondept, invoked for a device URI of canondept://host:9100. It builds the PJL header and then — the part I'd recommend to anyone writing one — pipes the result straight into the stock socket backend rather than opening its own TCP connection:

{
    printf '%s%%-12345X@PJL JOB NAME="%s"\r\n' "$ESC" "$jobname"
    printf '@PJL COMMENT CANPJL SET USERID = %s\r\n' "$deptid"
    printf '@PJL COMMENT CANPJL SET USERPWD = %s\r\n' "$pin"
    printf '@PJL ENTER LANGUAGE = PDF\r\n'
    if [ -n "$file" ]; then cat "$file"; else cat; fi
    printf '%s%%-12345X@PJL EOJ\r\n%s%%-12345X' "$ESC" "$ESC"
} | DEVICE_URI="socket://$host:$port" /usr/lib/cups/backend/socket \
        "$job" "$user" "$title" "$copies" "$opts"

Retry logic, timeouts, and connection handling are someone else's solved problem. Forty lines of my shell script would be a worse version of it.

Two details that are easy to get wrong, and both bite silently:

The credentials do not go in the device URI. It is tempting to write canondept://host?pin=1234 and read it back from $DEVICE_URI. Don't: lpstat -v is readable by every local user, so that publishes the PIN to anyone with a shell on the box. A separate 0600 root:root file keyed by hostname costs ten lines and keeps it out of reach.

The backend must be 0700 root:root. This one is genuinely surprising. CUPS decides whether to run a backend as root by looking at its permissions — if the file is readable by group or other, it drops to the lp user instead. A backend that needs to read a root-only credentials file therefore must be mode 0700, or it will start up fine and fail to find credentials it can see perfectly well from your shell. The permission bits are load-bearing.

Also worth knowing: CUPS enumerates backends at startup only, so a newly installed one is invisible until systemctl restart cups.

What this setup does not give you

The Generic PDF PPD covers duplex, colour, paper size and resolution. It does not cover finishing — no stapling, no hole-punch, no secure print, none of the things a copier of this class can physically do. Those live in Canon's UFR II driver (cnrdrvcups-lb in the AUR), which also has a proper Department ID field and would make the whole backend unnecessary. It is a much heavier install, and if all you want is paper with your document on it, the generic route is fine.

One last thing, which is not a technical caveat but is worth saying out loud: Department ID accounting means the copier logs who printed what. The backend passes the job title and username along via @JOBN and @USER, because that's the point of the feature. Whoever administers that machine can read your filenames. Rename the document before printing anything you'd rather they didn't.

The takeaway

I'd have found this in ten minutes with a driver that surfaced the error. Instead I had a queue reporting perfect health while the device quietly threw everything away, and the only way through was to stop believing the host and ask the hardware.

Any time you're printing over raw 9100, the queue's status tells you about your network stack and nothing else. The page counter is the ground truth. Learn the OID, it's worth the seven seconds:

snmpget -v1 -c public -Ovq <printer> 1.3.6.1.2.1.43.10.2.1.4.1.1