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
@@ -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();@@ -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());+}