feat(linux): add no-output test capture printer
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
This commit is contained in:
parent
eddcdd6af6
commit
cc9031fcfd
@ -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.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fieldlens/print",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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<AtomicBool>,
|
||||
@ -101,6 +103,12 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
let sink = TcpListener::bind(TEST_CAPTURE_SINK_ADDR)
|
||||
.with_context(|| format!("bind test-capture sink {TEST_CAPTURE_SINK_ADDR}"))?;
|
||||
sink.set_nonblocking(true)?;
|
||||
let worker_stop = stop.clone();
|
||||
handles.push(thread::spawn(move || test_capture_sink_loop(sink, worker_stop)));
|
||||
}
|
||||
|
||||
if handles.is_empty() {
|
||||
@ -110,6 +118,37 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
||||
Ok(CaptureRuntime { stop, handles })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_capture_sink_loop(listener: TcpListener, stop: Arc<AtomicBool>) {
|
||||
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,
|
||||
|
||||
@ -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<Vec<SystemPrinter>> {
|
||||
@ -389,7 +407,7 @@ fn linux_virtual_printer_status() -> Result<VirtualPrinterStatus> {
|
||||
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<VirtualPrinterStatus> {
|
||||
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();
|
||||
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()) {
|
||||
return Ok(());
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = Command::new("pkexec")
|
||||
.args(["lpadmin", "-x", "FieldLens-Print"])
|
||||
.arg("lpadmin")
|
||||
.args(&args)
|
||||
.status()
|
||||
.context("run elevated CUPS printer removal")?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow!("removing FieldLens Print from CUPS failed"));
|
||||
return Err(anyhow!("removing FieldLens CUPS printer '{queue}' failed"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = `
|
||||
<div>
|
||||
<strong>System printer</strong>
|
||||
<span id="virtual-printer-state" class="muted">Checking whether FieldLens Print is installed…</span>
|
||||
<small class="muted">Creates a printer named <b>FieldLens Print</b> in the normal OS print dialog and routes it into the local RAW listener.</small>
|
||||
<small class="muted">Linux creates <b>FieldLens Print</b> for real printing and <b>FieldLens Test Capture</b> for a full capture/render test with no physical output.</small>
|
||||
</div>
|
||||
<div class="actions printer-actions">
|
||||
<button class="btn" id="refresh-printers">Refresh printers</button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user