From cc9031fcfd2f1419bfbe537c117c81f014bdf4d3 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 24 Sep 2026 03:56:02 +0000 Subject: [PATCH] feat(linux): add no-output test capture printer --- README.md | 2 + package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/capture.rs | 39 +++++++++++++++++ src-tauri/src/printers.rs | 88 +++++++++++++++++++++++++++++---------- src-tauri/tauri.conf.json | 2 +- src/main.ts | 4 +- 7 files changed, 111 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ed9766f..6c997a0 100644 --- a/README.md +++ b/README.md @@ -58,4 +58,6 @@ Windows creates the virtual queue with the selected destination printer's existi Linux creates a CUPS queue named `FieldLens-Print` using the selected physical printer's capabilities and device URI. A FieldLens CUPS PDF pre-filter copies the original submitted PDF into `/var/spool/fieldlens-print` before the physical printer driver renders it, then passes the PDF onward unchanged through the normal CUPS filter chain. FieldLens Print watches that spool for capture/decoding while CUPS continues printing normally. +On Linux the same repair action also installs **FieldLens Test Capture**. It uses the same PPD and FieldLens pre-filter, but its rendered output is sent to a local discard sink at `127.0.0.1:9101`. Selecting that queue from a browser exercises original-document capture, spool ingestion, decoding, and the normal printer render/filter chain without sending anything to physical hardware. + USB/shared queues may not expose a RAW/TCP address. In that case the installed-printer selector still supplies the driver for the virtual Windows queue, while the physical destination address must be entered manually. diff --git a/package.json b/package.json index 655de9e..78c3531 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fieldlens/print", - "version": "0.1.7", + "version": "0.1.8", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 216b3a2..57b34e8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fieldlens-print" -version = "0.1.7" +version = "0.1.8" description = "FieldLens print-stream barcode capture companion" authors = ["Arrowstack"] edition = "2021" diff --git a/src-tauri/src/capture.rs b/src-tauri/src/capture.rs index dab640f..efd934a 100644 --- a/src-tauri/src/capture.rs +++ b/src-tauri/src/capture.rs @@ -26,6 +26,8 @@ use crate::{ #[cfg(target_os = "linux")] const SYSTEM_CUPS_SPOOL_DIR: &str = "/var/spool/fieldlens-print"; +#[cfg(target_os = "linux")] +const TEST_CAPTURE_SINK_ADDR: &str = "127.0.0.1:9101"; pub struct CaptureRuntime { stop: Arc, @@ -101,6 +103,12 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result Result) { + while !stop.load(Ordering::SeqCst) { + match listener.accept() { + Ok((stream, _)) => { + thread::spawn(move || drain_test_capture_stream(stream)); + } + Err(error) if error.kind() == ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(80)); + } + Err(_) => { + thread::sleep(Duration::from_millis(250)); + } + } + } +} + +#[cfg(target_os = "linux")] +fn drain_test_capture_stream(mut stream: TcpStream) { + let _ = stream.set_read_timeout(Some(Duration::from_secs(2))); + let mut buffer = [0u8; 32 * 1024]; + loop { + match stream.read(&mut buffer) { + Ok(0) => break, + Ok(_) => {} + Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => break, + Err(_) => break, + } + } +} + fn raw_proxy_loop( listener: TcpListener, config: AppConfig, diff --git a/src-tauri/src/printers.rs b/src-tauri/src/printers.rs index aadf413..e732bdb 100644 --- a/src-tauri/src/printers.rs +++ b/src-tauri/src/printers.rs @@ -11,6 +11,12 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; pub const VIRTUAL_PRINTER_NAME: &str = "FieldLens Print"; +#[cfg(target_os = "linux")] +pub const TEST_VIRTUAL_PRINTER_NAME: &str = "FieldLens Test Capture"; +#[cfg(target_os = "linux")] +const TEST_VIRTUAL_PRINTER_QUEUE: &str = "FieldLens-Test-Capture"; +#[cfg(target_os = "linux")] +const TEST_CAPTURE_DEVICE_URI: &str = "socket://127.0.0.1:9101"; const WINDOWS_PORT_NAME: &str = "FieldLensPrint-9100"; #[cfg(target_os = "linux")] @@ -108,8 +114,20 @@ pub struct VirtualPrinterStatus { } fn is_virtual_printer_name(name: &str) -> bool { - name.eq_ignore_ascii_case(VIRTUAL_PRINTER_NAME) + if name.eq_ignore_ascii_case(VIRTUAL_PRINTER_NAME) || name.eq_ignore_ascii_case("FieldLens-Print") + { + return true; + } + + #[cfg(target_os = "linux")] + { + return name.eq_ignore_ascii_case(TEST_VIRTUAL_PRINTER_NAME) + || name.eq_ignore_ascii_case(TEST_VIRTUAL_PRINTER_QUEUE); + } + + #[cfg(not(target_os = "linux"))] + false } pub fn list_system_printers() -> Result> { @@ -389,7 +407,7 @@ fn linux_virtual_printer_status() -> Result { let printers = list_linux_printers()?; let installed = printers .iter() - .any(|printer| is_virtual_printer_name(&printer.name)); + .any(|printer| printer.name.eq_ignore_ascii_case("FieldLens-Print")); if !installed { return Ok(VirtualPrinterStatus { @@ -420,7 +438,7 @@ fn linux_virtual_printer_status() -> Result { installed: ready, name: VIRTUAL_PRINTER_NAME.into(), detail: if ready { - "Ready. Original PDF jobs are captured before the physical printer driver renders them.".into() + "Ready. FieldLens Print sends to the physical printer; FieldLens Test Capture runs the same capture/render path but discards the rendered output.".into() } else { "FieldLens Print exists but CUPS has it disabled or not accepting jobs. Click Create / repair FieldLens Print.".into() }, @@ -556,7 +574,8 @@ fn install_linux_virtual_printer(target_printer: &str) -> Result<()> { install_linux_capture_filter()?; let ppd_path = build_fieldlens_ppd(target_printer)?; - let args = vec![ + + let production_args = vec![ "-p".to_string(), "FieldLens-Print".to_string(), "-E".to_string(), @@ -567,34 +586,57 @@ fn install_linux_virtual_printer(target_printer: &str) -> Result<()> { "-D".to_string(), VIRTUAL_PRINTER_NAME.to_string(), ]; + run_linux_admin( + "lpadmin", + &production_args, + "configure FieldLens production CUPS printer", + )?; - let setup_result = run_linux_admin("lpadmin", &args, "configure FieldLens CUPS printer"); + let test_args = vec![ + "-p".to_string(), + TEST_VIRTUAL_PRINTER_QUEUE.to_string(), + "-E".to_string(), + "-v".to_string(), + TEST_CAPTURE_DEVICE_URI.to_string(), + "-P".to_string(), + ppd_path.to_string_lossy().into_owned(), + "-D".to_string(), + TEST_VIRTUAL_PRINTER_NAME.to_string(), + ]; + let test_result = run_linux_admin( + "lpadmin", + &test_args, + "configure FieldLens test-capture CUPS printer", + ); let _ = fs::remove_file(&ppd_path); - setup_result?; + test_result?; - // lpadmin -E normally enables the queue, but explicitly repair both CUPS - // state flags because Chromium disables Print when either is stale. - run_cups_repair_command("cupsenable", &["FieldLens-Print"])?; - run_cups_repair_command("cupsaccept", &["FieldLens-Print"])?; + // Explicitly repair both CUPS state flags because Chromium disables Print + // when either is stale. + for queue in ["FieldLens-Print", TEST_VIRTUAL_PRINTER_QUEUE] { + run_cups_repair_command("cupsenable", &[queue])?; + run_cups_repair_command("cupsaccept", &[queue])?; + } Ok(()) } #[cfg(target_os = "linux")] fn remove_linux_virtual_printer() -> Result<()> { - let direct = Command::new("lpadmin") - .args(["-x", "FieldLens-Print"]) - .status(); - if matches!(direct, Ok(status) if status.success()) { - return Ok(()); - } + for queue in ["FieldLens-Print", TEST_VIRTUAL_PRINTER_QUEUE] { + let args = vec!["-x".to_string(), queue.to_string()]; + let direct = Command::new("lpadmin").args(&args).status(); + if matches!(direct, Ok(status) if status.success()) { + continue; + } - let status = Command::new("pkexec") - .args(["lpadmin", "-x", "FieldLens-Print"]) - .status() - .context("run elevated CUPS printer removal")?; - - if !status.success() { - return Err(anyhow!("removing FieldLens Print from CUPS failed")); + let status = Command::new("pkexec") + .arg("lpadmin") + .args(&args) + .status() + .context("run elevated CUPS printer removal")?; + if !status.success() { + return Err(anyhow!("removing FieldLens CUPS printer '{queue}' failed")); + } } Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bcae026..fc44dad 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "FieldLens Print", - "version": "0.1.7", + "version": "0.1.8", "identifier": "dev.arrowstack.fieldlens.print", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/main.ts b/src/main.ts index f87d016..9606000 100644 --- a/src/main.ts +++ b/src/main.ts @@ -85,7 +85,7 @@ let config: AppConfig let jobs: Job[] = [] let status: Status let loginTimer: number | undefined -let currentVersion = '0.1.7' +let currentVersion = '0.1.8' let bundleType: BundleType | null = null let availableUpdate: Update | null = null let availableManualUpdate: ManualUpdate | null = null @@ -138,7 +138,7 @@ app.innerHTML = `
System printer Checking whether FieldLens Print is installed… - Creates a printer named FieldLens Print in the normal OS print dialog and routes it into the local RAW listener. + Linux creates FieldLens Print for real printing and FieldLens Test Capture for a full capture/render test with no physical output.