collective

Browse / github.com/storytold/craft-libs / patch

craft-print: list Windows printers without reading every driver

craft-print: list Windows printers without reading every driver Fixes the Windows half of storytold/pdfcraft#2192. `printers()` read each driver's PrintCapabilities to fill `Printer::two_sided`. Windows takes about 48 s to give up on a printer it can't reach, such as a WSD printer on another network, and the reads ran one after another, so the list waited on every such printer before anything was shown. Timing `Get-PrintConfiguration` per printer on an affected laptop: | Printer port | Seconds | |---|---| | WSD, unreachable | 48.1 | | WSD, unreachable | 47.9 | | WSD, unreachable | 47.0 | | TCP/IP | 0.2 | | Virtual printers (3) | 0.1 each | With three un

Status
open
Score
0.00
Adopters
0 other operators
Author
art804 (imported from pull request #133, open)
Base
6a66b95cbc2c4d4b732630113eb9a37263658c58
Size
+43 −13 in 4 files
Review
pending
Shared
2026-10-28
Id
2f4f240d171deb6b1bf9a01e

How it went

No other operator has reported using it yet.

Diff

raw · crates/print/README.md crates/print/src/lib.rs crates/print/src/tests/windows_native.rs crates/print/src/windows.rs

diff --git a/crates/print/README.md b/crates/print/README.md--- a/crates/print/README.md+++ b/crates/print/README.md@@ -117,8 +117,9 @@ It runs formatting, Clippy and this crate's own tests on Windows, Linux and macO code changes, and supports manual runs. The Windows job executes the spooler regression above; physical printer fidelity still requires hardware verification. -Windows discovery reports driver duplex support in `Printer::two_sided` (a two-sided option in-the driver's PrintCapabilities); CUPS leaves it unknown. Explicit simplex/duplex choices that the driver+Discovery leaves `Printer::two_sided` unknown, so listing printers never waits on a driver.+On Windows, `two_sided(printer)` reads one printer's driver duplex support (a two-sided option in+its PrintCapabilities); elsewhere it returns `None`. Explicit simplex/duplex choices that the driver drops are refused before submission.  Windows dependencies: the maintained [`winprint` fork](https://github.com/storytold/winprint.rs)diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs--- a/crates/print/src/lib.rs+++ b/crates/print/src/lib.rs@@ -49,7 +49,8 @@ pub struct Printer {     /// `_192_0_2_15` described as `192.0.2.15`. Windows names are already human: empty there.     pub description: String,     pub default: bool,-    /// Whether the Windows driver reports duplex support; None when unknown (CUPS).+    /// Whether the Windows driver reports duplex support; None when unknown (CUPS, and+    /// [`printers`] on Windows, which doesn't read it: see [`two_sided`]).     pub two_sided: Option<bool>, } @@ -411,6 +412,23 @@ pub fn printers() -> Vec<Printer> {     } } +/// Whether `printer`'s driver takes two-sided settings from other programs (Windows: its+/// PrintCapabilities offer a two-sided option); `None` when that can't be read, or off Windows.+/// [`printers`] leaves [`Printer::two_sided`] unknown, because reading the capabilities of a+/// printer that can't be reached, such as a WSD printer on another network, takes Windows about+/// 48 s. Ask for the printer the user picks, 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 comesdiff --git a/crates/print/src/tests/windows_native.rs b/crates/print/src/tests/windows_native.rs--- a/crates/print/src/tests/windows_native.rs+++ b/crates/print/src/tests/windows_native.rs@@ -410,6 +410,7 @@ fn windows_jobs_reach_the_spooler() {         return;     }     let listed = spool::printers().into_iter().find(|p| p.name == name);+    let duplex = spool::two_sided(name);     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();@@ -422,7 +423,7 @@ fn windows_jobs_reach_the_spooler() {         observed.borrow_mut().push((done, total));     });     let mut duplex_progress = Vec::new();-    let duplex_refused = if listed.as_ref().is_some_and(|p| p.two_sided == Some(false)) {+    let duplex_refused = if duplex == Some(false) {         Some(spool::submit_with_progress(&mixed, &Job { duplex: Some(Duplex::LongEdge), ..job.clone() }, &mut |done, total| {             duplex_progress.push((done, total));         }))@@ -430,7 +431,7 @@ fn windows_jobs_reach_the_spooler() {         None     };     // The Print dialog's default, an explicit one-sided request, on a queue with no duplex unit.-    let simplex_only = listed.as_ref().is_some_and(|p| p.two_sided == Some(false));+    let simplex_only = duplex == Some(false);     let one_sided = simplex_only.then(|| spool::submit(&mixed, &Job { duplex: Some(Duplex::Off), title: "simplex".into(), ..job.clone() }));     let tabloid = test_pdf(&[TABLOID, LETTER], false);     let shrink = Job { printer: Some(name.into()), scaling: Scaling::ShrinkToFit, title: "shrink".into(), ..Job::default() };@@ -449,7 +450,8 @@ fn windows_jobs_reach_the_spooler() {         "Get-PrintJob -PrinterName $name | Remove-PrintJob; foreach ($i in 1..20) { if (-not (Get-PrintJob -PrinterName $name)) { break }; Start-Sleep -Milliseconds 250 }; Remove-Printer -Name $name; if (Get-Printer -Name $name -ErrorAction SilentlyContinue) { throw 'the printer is still there' }; exit 0",         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!(listed.as_ref().is_some_and(|p| p.two_sided.is_none()), "the new printer is listed without reading its driver: {listed:?}");+    assert!(duplex.is_some(), "its duplex support is known when asked for: {duplex:?}");     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:?}");diff --git a/crates/print/src/windows.rs b/crates/print/src/windows.rs--- a/crates/print/src/windows.rs+++ b/crates/print/src/windows.rs@@ -3,12 +3,12 @@ //! PowerShell in Constrained Language Mode; storytold/pdfcraft#1107). //! //! The printers come from `EnumPrinters` (local queues and the user's connections, such as-//! `\\server\queue`), the default from `GetDefaultPrinter`, and whether each driver takes-//! two-sided settings from its PrintCapabilities. A job is drawn by the in-box-//! `Windows.Data.Pdf` with Direct2D into the XPS print API, as vectors, and its options (copies,-//! collation, two-sided, colour, paper, orientation, no driver scaling) travel as a PrintTicket-//! that the driver validates before anything is sent. `winprint` and `winsafe` wrap the Win32-//! and WinRT calls, so there is no `unsafe` here.+//! `\\server\queue`) and the default from `GetDefaultPrinter`; whether a driver takes two-sided+//! settings comes from its PrintCapabilities, read only for the printer asked about. A job is+//! drawn by the in-box `Windows.Data.Pdf` with Direct2D into the XPS print API, as vectors, and+//! its options (copies, collation, two-sided, colour, paper, orientation, no driver scaling)+//! travel as a PrintTicket that the driver validates before anything is sent. `winprint` and+//! `winsafe` wrap the Win32 and WinRT calls, so there is no `unsafe` here.  use std::io::Write as _; @@ -29,13 +29,22 @@ fn default_printer() -> Option<String> {     winsafe::GetDefaultPrinter().ok().filter(|n| !n.is_empty()) } +/// The queues and the default only. Each driver's capabilities are left unread: Windows takes+/// about 48 s to give up on a printer it can't reach (a WSD printer on another network), and+/// reading every printer's made the list wait for each one in turn (storytold/pdfcraft#2192).+/// [`two_sided_named`] reads them for the printer that is picked. pub(crate) fn printers() -> 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()),+        Ok(devices) => crate::printer_list(devices.iter().map(|d| (d.name().to_string(), None)), default_printer().as_deref()),         Err(_) => Vec::new(),     } } +/// [`two_sided`] for the queue named `name`; `None` when there is no such queue.+pub(crate) fn two_sided_named(name: &str) -> Option<bool> {+    PrinterDevice::all().ok()?.iter().find(|d| d.name() == name).and_then(two_sided)+}+ /// 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.