Merge pull request #618 from arnav-hiwarkar/fix/print-queues-without-driver-queries

craft-print: printer_queues lists the queues without asking any driver
This commit is contained in:
FredApps authored and GitHub committed 2026-11-06 01:35:22 +01:00
commit e653048a18
4 files changed
+54 -1

No files matched your search

+4 -1
View File
@@ -37,7 +37,10 @@ opens Windows printer preferences and submits jobs with copies, collation, duple
- **Windows:** in this process, with no PowerShell, script or other program started, so
printing works where App Control for Business or AppLocker enforce script rules. Printers
come from `EnumPrinters` (local queues and `\\server\queue` connections) and the default from
`GetDefaultPrinter`. A job is drawn as vectors by the in-box `Windows.Data.Pdf` with Direct2D
`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 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,
orientation and "no scaling" travel in a PrintTicket that the driver validates first; a
driver that would change the paper, orientation or scaling is refused before anything is
+16
View File
@@ -422,6 +422,22 @@ pub fn printers() -> Vec<Printer> {
}
}
/// The printers the system knows, without asking their drivers anything: [`printers`] with
/// `two_sided` unknown. On Windows [`printers`] reads each driver's PrintCapabilities, which can
/// wait on the device (a networked printer in power save), so a Print dialog that only needs
/// names lists them from this without that wait. Submission still checks the job's options with
/// the driver. Elsewhere it is [`printers`], whose `two_sided` is already unknown.
pub fn printer_queues() -> Vec<Printer> {
#[cfg(windows)]
{
windows::printer_queues()
}
#[cfg(not(windows))]
{
printers()
}
}
/// 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
+11
View File
@@ -409,7 +409,15 @@ 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);
// 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 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();
@@ -450,6 +458,9 @@ fn windows_jobs_reach_the_spooler() {
name,
);
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");
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:?}");
+23
View File
@@ -43,16 +43,39 @@ fn print_ticket_provider() -> MutexGuard<'static, ()> {
}
pub(crate) fn printers() -> Vec<Printer> {
spooler_list(two_sided)
}
/// The queues and the default without asking any driver: a networked printer asleep in power
/// save can hold its driver's PrintCapabilities query for a long time, and the Print dialog
/// must not wait on it to show the queue (or any other).
pub(crate) fn printer_queues() -> Vec<Printer> {
spooler_list(|_| None)
}
/// The spooler's queues (`EnumPrinters`, local queues and connections) with the default marked,
/// each given the two-sided support `two_sided` reports for it; empty when the spooler can't list
/// them.
fn spooler_list(two_sided: impl Fn(&PrinterDevice) -> Option<bool>) -> Vec<Printer> {
match PrinterDevice::all() {
Ok(devices) => crate::printer_list(devices.iter().map(|d| (d.name().to_string(), two_sided(d))), default_printer().as_deref()),
Err(_) => Vec::new(),
}
}
#[cfg(test)]
thread_local! {
/// The driver capability queries printer discovery made on this thread, so a test can show
/// that [`printer_queues`] makes none.
pub(crate) static CAPABILITY_QUERIES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Whether the printer's driver takes two-sided settings: its PrintCapabilities offer a two-sided
/// option for the feature the job's ticket sets. `None` when the capabilities can't be read. A
/// driver without one cannot confirm an explicit two-sided request, which is refused before submission.
fn two_sided(device: &PrinterDevice) -> Option<bool> {
#[cfg(test)]
CAPABILITY_QUERIES.with(|n| n.set(n.get().saturating_add(1)));
let capabilities = {
let _provider = print_ticket_provider();
PrintCapabilities::fetch(device)