collective

Browse / github.com/storytold/pdfcraft / patch

fix(ui): prevent print dialog from hanging indefinitely on printer search (#5020)

Print dialog stuck on "Searching for printers…" forever on Windows (v0.14.0) When I open the Print dialog (Ctrl+P or the printer icon), the **Printer** dropdown stays on "Buscando impresoras…" ("Searching for printers…") indefinitely. I waited more than 30 seconds several times and the list never loads, so I cannot print any document. The **Print** button stays unusable because no printer is selected. Windows itself lists all printers correctly in Settings → Bluetooth & devices → Printers & scanners, and other applications can print, so the Windows print spooler is working. <img width="865" height="734" alt="Image" src="https://github.com/user-attachments/assets/f6d5

Status
open
Score
0.00
Adopters
0 other operators
Author
YusufMntm (imported from pull request #5045, open)
Base
e9fc890a862ea2d8c895ea32ddecda98580483cd
Size
+62 −3 in 2 files
Review
pending
Shared
2026-10-28
Id
a5aba8dfa64355cbf7f16b51

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/workflows/print_ui.rs crates/ui-egui/tests/it/print.rs

diff --git a/crates/ui-egui/src/workflows/print_ui.rs b/crates/ui-egui/src/workflows/print_ui.rs--- a/crates/ui-egui/src/workflows/print_ui.rs+++ b/crates/ui-egui/src/workflows/print_ui.rs@@ -38,12 +38,16 @@ pub fn system_printers() -> PrinterSource {     std::sync::Arc::new(spool::printers) } +/// Maximum duration to wait for the system spooler to return printers before timing out (#5020).+pub const PRINTER_SEARCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);+ /// The Print dialog's work in the background: the printer list being read, and the jobs being /// spooled, oldest first. #[derive(Default)] pub(crate) struct PrintTasks {+    /// Background receiver for the printer list, paired with the start instant for timeout handling.     #[cfg(not(target_arch = "wasm32"))]-    printers: Option<std::sync::mpsc::Receiver<Vec<spool::Printer>>>,+    printers: Option<(std::sync::mpsc::Receiver<Vec<spool::Printer>>, std::time::Instant)>,     #[cfg(not(target_arch = "wasm32"))]     jobs: Vec<PrintJob>,     /// The progress card is showing a print job (it is shared with other long jobs).@@ -218,6 +222,7 @@ impl PdfCraftApp {         self.print_draft.crop_pages = Some(pages);     } +    /// Dispatches a background thread to discover available printers from `source` (#5020).     #[cfg(not(target_arch = "wasm32"))]     fn list_printers_in_background(&mut self, source: PrinterSource) {         // A list already being read is as fresh as a new one would be.@@ -232,7 +237,7 @@ impl PdfCraftApp {             // The receiver may be gone (the app quit): nothing to report to then.             let _ = tx.send(source());         });-        self.print_tasks.printers = Some(rx);+        self.print_tasks.printers = Some((rx, std::time::Instant::now()));     }      /// Read `printer`'s driver options off the GUI thread (#1818); `poll_print` picks them up.@@ -299,9 +304,14 @@ impl PdfCraftApp {             for (printer, options) in arrived {                 self.receive_options(printer, options);             }-            if let Some(rx) = &self.print_tasks.printers {+            if let Some((rx, started)) = &self.print_tasks.printers {+                let timeout = started.elapsed() >= PRINTER_SEARCH_TIMEOUT;                 let printers = match rx.try_recv() {                     Ok(p) => Some(p),+                    Err(std::sync::mpsc::TryRecvError::Empty) if timeout => {+                        // The search took too long (e.g. hung Windows spooler or offline network printer; #5020).+                        Some(self.print_draft.printers.clone())+                    }                     Err(std::sync::mpsc::TryRecvError::Empty) => None,                     // A source that died lists nothing new; the dialog keeps what it has.                     Err(std::sync::mpsc::TryRecvError::Disconnected) => Some(self.print_draft.printers.clone()),@@ -316,6 +326,9 @@ impl PdfCraftApp {                     }                     d.printers = printers;                     d.printers_loading = false;+                } else if let Some(ctx) = &self.ctx {+                    let remaining = PRINTER_SEARCH_TIMEOUT.saturating_sub(started.elapsed());+                    ctx.request_repaint_after(remaining);                 }             }             let mut finished = Vec::new();diff --git a/crates/ui-egui/tests/it/print.rs b/crates/ui-egui/tests/it/print.rs--- a/crates/ui-egui/tests/it/print.rs+++ b/crates/ui-egui/tests/it/print.rs@@ -913,3 +913,49 @@ fn a_booklet_decides_two_sided_itself() {     assert!(h.query_by_role_and_label(Role::ComboBox, "Two-sided:").is_some());     assert_eq!(h.state().print_draft.job("x").duplex, Some(Duplex::LongEdge), "back to the choice"); }++/// #5020: if printer scanning hangs (or takes longer than 3 seconds), the dialog+/// does not get stuck: Save as PDF can be picked immediately while looking, and+/// printers_loading times out after 3 seconds.+#[test]+fn hung_printer_source_times_out_and_allows_save_as_pdf() {+    let mut h = harness();+    h.state_mut().printer_source = Some(std::sync::Arc::new(|| {+        std::thread::sleep(std::time::Duration::from_secs(10));+        Vec::new()+    }));+    assert!(h.state_mut().execute("print.dialog"));+    h.run_steps(3);+    assert_eq!(h.state().dialog, Some(Dialog::Print));+    // Printer search is active initially.+    assert!(h.state().print_draft.printers_loading);+    assert_eq!(h.state().print_draft.printer, None);+    // Choosing Save as PDF while looking makes it clickable immediately without waiting.+    h.state_mut().print_draft.printer_picked = true;+    h.run_steps(1);+    let out = std::env::temp_dir().join(format!("pdfcraft-print-hung-{}.pdf", std::process::id()));+    h.state_mut().save_override = Some(out.to_string_lossy().into_owned());+    h.get_by_label("Save as PDF").click();+    h.run_steps(3);+    assert_eq!(h.state().dialog, None);+    let _ = std::fs::remove_file(out);+}++/// #5020: printer search times out after 3 seconds, clears printers_loading and defaults to Save as PDF.+#[test]+fn hung_printer_source_resets_printers_loading_on_timeout() {+    let mut h = harness();+    h.state_mut().printer_source = Some(std::sync::Arc::new(|| {+        std::thread::sleep(std::time::Duration::from_secs(10));+        Vec::new()+    }));+    assert!(h.state_mut().execute("print.dialog"));+    h.run_steps(3);+    assert!(h.state().print_draft.printers_loading);+    // Advance past the 3-second timeout.+    std::thread::sleep(std::time::Duration::from_millis(3100));+    h.run_steps(2);+    assert!(!h.state().print_draft.printers_loading);+    // Save as PDF is now offered and enabled.+    assert!(h.query_by_role_and_label(Role::Button, "Save as PDF").is_some());+}