craft-print: two_sided(printer) reads one printer's two-sided support (#133)

printer_queues() (#618) lists the queues without asking any driver, so
every printer's two_sided is unknown there. A Print dialog listing from it
still needs to know whether the printer the user picks takes two-sided
settings, without asking every other driver as printers() does: Windows
takes about 48 s to give up on a printer it can't reach, such as a WSD
printer on another network (storytold/pdfcraft#2192).

two_sided(printer) finds that queue and reads its driver's capabilities
with the same check printers() makes, so the answers match. It is None
when there is no such queue, when the capabilities can't be read, and off
Windows, as printers() reports there.

The paused-printer test now checks that two_sided(name) gives printers()'s
answer for the test queue from exactly one driver query, and a new test
checks that a name no queue has is unknown. The query counting moves into
a small helper, which keeps windows_jobs_reach_the_spooler under the
cognitive-complexity limit.

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
art804
2026-11-10 00:03:35 +01:00
committed by GitHub
co-authored by FredApps
parent 92451193ba
commit b6b8a55b2c
4 changed files with 54 additions and 9 deletions
+2 -1
View File
@@ -40,7 +40,8 @@ opens Windows printer preferences and submits jobs with copies, collation, duple
`GetDefaultPrinter`. `printers` also reads each driver's PrintCapabilities to report two-sided
support, which can wait on the device (a networked printer in power save); `printer_queues`
lists the same queues without asking any driver, so a Print dialog's list never waits on a
device. A job asks the driver too before it is sent; `submit_cancellable` with a
device, and `two_sided(name)` asks the driver of just the printer picked. A job asks the
driver too before it is sent; `submit_cancellable` with a
`Cancel` token lets the app stop a job still waiting on it, and a cancelled job is never
sent. A job is drawn as vectors by the in-box `Windows.Data.Pdf` with Direct2D
into the XPS print API (the `winprint` crate), and copies, collation, duplex, colour, paper,
+17
View File
@@ -438,6 +438,23 @@ pub fn printer_queues() -> Vec<Printer> {
}
}
/// Whether `printer`'s driver takes two-sided settings, read for that one printer: the answer
/// [`printers`] gives for it, without asking every other driver too. On Windows that is its
/// PrintCapabilities; `None` when they can't be read or no queue has that name. Elsewhere `None`,
/// as [`printers`] reports. It can wait on the device, so a Print dialog listing from
/// [`printer_queues`] asks it for the printer picked, off the UI thread.
pub fn two_sided(printer: &str) -> Option<bool> {
#[cfg(windows)]
{
windows::two_sided_named(printer)
}
#[cfg(not(windows))]
{
let _ = printer;
None
}
}
/// The Print dialog's printers from lpstat's two answers: the spooler's queues (`-p -d`), then
/// the driverless destinations of `available` (`-e`) that have no queue — CUPS builds a temporary
/// queue when `lp` sends them a job. Each destination once, queues first; the default still comes
+30 -8
View File
@@ -322,6 +322,14 @@ fn an_encrypted_file_is_turned_but_not_reframed() {
assert!(matches!(sheets.update(&shrunk), Err(Error::Spool(ref m)) if m.contains("encrypted")));
}
#[test]
fn two_sided_is_unknown_for_a_printer_that_is_not_there() {
// A name no queue has, as when a printer was removed after the list was read: unknown, at
// once, never an error or a guess.
assert_eq!(spool::two_sided("craft-print: no such printer \u{1F5A8}"), None);
assert_eq!(spool::two_sided(""), None);
}
#[test]
fn unreadable_sheets_are_errors_never_panics() {
for bad in [&b""[..], b"not a pdf", b"%PDF-1.7\nstartxref\n999999\n%%EOF", b"%PDF-1.7\nxref\n0 1\ntrailer\n<< >>\nstartxref\n9\n%%EOF"] {
@@ -354,6 +362,22 @@ fn unreadable_sheets_are_errors_never_panics() {
}
}
/// What `f` returns, and how many driver capability queries it made on this thread.
#[cfg(windows)]
fn driver_queries<T>(f: impl FnOnce() -> T) -> (T, usize) {
let queries = || crate::windows::CAPABILITY_QUERIES.with(std::cell::Cell::get);
let start = queries();
let out = f();
(out, queries().saturating_sub(start))
}
/// `two_sided(name)` and the driver queries it made, beside what it should be: the answer
/// `printers()` listed for that queue, from one query.
#[cfg(windows)]
fn picked_printer(name: &str, listed: Option<&Printer>) -> ((Option<bool>, usize), (Option<bool>, usize)) {
(driver_queries(|| spool::two_sided(name)), (listed.and_then(|p| p.two_sided), 1))
}
/// Windows: the printer list comes from the spooler. A bare machine may have no printer; the
/// list that exists has real names and at most one default.
#[cfg(windows)]
@@ -449,15 +473,12 @@ fn windows_jobs_reach_the_spooler() {
eprintln!("skipping the real print: the test printer could not be prepared: {why}");
return;
}
let queries = || crate::windows::CAPABILITY_QUERIES.with(std::cell::Cell::get);
let start = queries();
let listed = spool::printers().into_iter().find(|p| p.name == name);
let probed = queries().saturating_sub(start);
let (listed, probed) = driver_queries(|| spool::printers().into_iter().find(|p| p.name == name));
// The list a Print dialog shows first: the same queue, named without asking its driver (a
// networked printer in power save can hold that query, storytold/wordcraft#5710).
let start = queries();
let queue = spool::printer_queues().into_iter().find(|p| p.name == name);
let unprobed = queries().saturating_sub(start);
let (queue, unprobed) = driver_queries(|| spool::printer_queues().into_iter().find(|p| p.name == name));
// Then the one printer picked: its driver alone, the same answer printers() gave.
let (picked, expected) = picked_printer(name, listed.as_ref());
let mixed = test_pdf(&[LETTER, LETTER_LANDSCAPE, LETTER], true);
let job = Job { printer: Some(name.into()), title: "mixed.pdf".into(), ..Job::default() };
let caller = std::thread::current().id();
@@ -502,7 +523,8 @@ fn windows_jobs_reach_the_spooler() {
assert!(listed.as_ref().is_some_and(|p| p.two_sided.is_some()), "the new printer is listed, its duplex support known: {listed:?}");
assert!(queue.as_ref().is_some_and(|p| p.two_sided.is_none()), "the new printer is listed, its duplex support unknown: {queue:?}");
assert!(probed > 0, "printers() asks each driver, so the count sees driver queries: {probed}");
assert_eq!(unprobed, 0, "printer_queues() asks no driver");
// printer_queues() asks no driver; two_sided(name) answers as printers() did, asking that one only.
assert_eq!((unprobed, picked), (0, expected));
assert_eq!(sent.as_deref(), Ok("spooled 3 sheet(s)"), "{sent:?}");
if let Some(refused) = duplex_refused {
assert!(matches!(refused, Err(Error::Spool(ref m)) if m.contains("simplex/duplex") && m.contains("nothing was sent")), "{refused:?}");
+5
View File
@@ -63,6 +63,11 @@ fn spooler_list(two_sided: impl Fn(&PrinterDevice) -> Option<bool>) -> Vec<Print
}
}
/// [`two_sided`] for the queue named `name` only; `None` when no queue has that name.
pub(crate) fn two_sided_named(name: &str) -> Option<bool> {
PrinterDevice::all().ok()?.iter().find(|d| d.name() == name).and_then(two_sided)
}
#[cfg(test)]
thread_local! {
/// The driver capability queries printer discovery made on this thread, so a test can show