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.
|
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.
|
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",
|
"name": "@fieldlens/print",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fieldlens-print"
|
name = "fieldlens-print"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
description = "FieldLens print-stream barcode capture companion"
|
description = "FieldLens print-stream barcode capture companion"
|
||||||
authors = ["Arrowstack"]
|
authors = ["Arrowstack"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@ -26,6 +26,8 @@ use crate::{
|
|||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
const SYSTEM_CUPS_SPOOL_DIR: &str = "/var/spool/fieldlens-print";
|
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 {
|
pub struct CaptureRuntime {
|
||||||
stop: Arc<AtomicBool>,
|
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() {
|
if handles.is_empty() {
|
||||||
@ -110,6 +118,37 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
Ok(CaptureRuntime { stop, handles })
|
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(
|
fn raw_proxy_loop(
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
|
|||||||
@ -11,6 +11,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
pub const VIRTUAL_PRINTER_NAME: &str = "FieldLens Print";
|
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";
|
const WINDOWS_PORT_NAME: &str = "FieldLensPrint-9100";
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -108,8 +114,20 @@ pub struct VirtualPrinterStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_virtual_printer_name(name: &str) -> bool {
|
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")
|
|| 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>> {
|
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 printers = list_linux_printers()?;
|
||||||
let installed = printers
|
let installed = printers
|
||||||
.iter()
|
.iter()
|
||||||
.any(|printer| is_virtual_printer_name(&printer.name));
|
.any(|printer| printer.name.eq_ignore_ascii_case("FieldLens-Print"));
|
||||||
|
|
||||||
if !installed {
|
if !installed {
|
||||||
return Ok(VirtualPrinterStatus {
|
return Ok(VirtualPrinterStatus {
|
||||||
@ -420,7 +438,7 @@ fn linux_virtual_printer_status() -> Result<VirtualPrinterStatus> {
|
|||||||
installed: ready,
|
installed: ready,
|
||||||
name: VIRTUAL_PRINTER_NAME.into(),
|
name: VIRTUAL_PRINTER_NAME.into(),
|
||||||
detail: if ready {
|
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 {
|
} else {
|
||||||
"FieldLens Print exists but CUPS has it disabled or not accepting jobs. Click Create / repair FieldLens Print.".into()
|
"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()?;
|
install_linux_capture_filter()?;
|
||||||
let ppd_path = build_fieldlens_ppd(target_printer)?;
|
let ppd_path = build_fieldlens_ppd(target_printer)?;
|
||||||
let args = vec![
|
|
||||||
|
let production_args = vec![
|
||||||
"-p".to_string(),
|
"-p".to_string(),
|
||||||
"FieldLens-Print".to_string(),
|
"FieldLens-Print".to_string(),
|
||||||
"-E".to_string(),
|
"-E".to_string(),
|
||||||
@ -567,34 +586,57 @@ fn install_linux_virtual_printer(target_printer: &str) -> Result<()> {
|
|||||||
"-D".to_string(),
|
"-D".to_string(),
|
||||||
VIRTUAL_PRINTER_NAME.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);
|
let _ = fs::remove_file(&ppd_path);
|
||||||
setup_result?;
|
test_result?;
|
||||||
|
|
||||||
// lpadmin -E normally enables the queue, but explicitly repair both CUPS
|
// Explicitly repair both CUPS state flags because Chromium disables Print
|
||||||
// state flags because Chromium disables Print when either is stale.
|
// when either is stale.
|
||||||
run_cups_repair_command("cupsenable", &["FieldLens-Print"])?;
|
for queue in ["FieldLens-Print", TEST_VIRTUAL_PRINTER_QUEUE] {
|
||||||
run_cups_repair_command("cupsaccept", &["FieldLens-Print"])?;
|
run_cups_repair_command("cupsenable", &[queue])?;
|
||||||
|
run_cups_repair_command("cupsaccept", &[queue])?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn remove_linux_virtual_printer() -> Result<()> {
|
fn remove_linux_virtual_printer() -> Result<()> {
|
||||||
let direct = Command::new("lpadmin")
|
for queue in ["FieldLens-Print", TEST_VIRTUAL_PRINTER_QUEUE] {
|
||||||
.args(["-x", "FieldLens-Print"])
|
let args = vec!["-x".to_string(), queue.to_string()];
|
||||||
.status();
|
let direct = Command::new("lpadmin").args(&args).status();
|
||||||
if matches!(direct, Ok(status) if status.success()) {
|
if matches!(direct, Ok(status) if status.success()) {
|
||||||
return Ok(());
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = Command::new("pkexec")
|
let status = Command::new("pkexec")
|
||||||
.args(["lpadmin", "-x", "FieldLens-Print"])
|
.arg("lpadmin")
|
||||||
.status()
|
.args(&args)
|
||||||
.context("run elevated CUPS printer removal")?;
|
.status()
|
||||||
|
.context("run elevated CUPS printer removal")?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(anyhow!("removing FieldLens Print from CUPS failed"));
|
return Err(anyhow!("removing FieldLens CUPS printer '{queue}' failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "FieldLens Print",
|
"productName": "FieldLens Print",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"identifier": "dev.arrowstack.fieldlens.print",
|
"identifier": "dev.arrowstack.fieldlens.print",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@ -85,7 +85,7 @@ let config: AppConfig
|
|||||||
let jobs: Job[] = []
|
let jobs: Job[] = []
|
||||||
let status: Status
|
let status: Status
|
||||||
let loginTimer: number | undefined
|
let loginTimer: number | undefined
|
||||||
let currentVersion = '0.1.7'
|
let currentVersion = '0.1.8'
|
||||||
let bundleType: BundleType | null = null
|
let bundleType: BundleType | null = null
|
||||||
let availableUpdate: Update | null = null
|
let availableUpdate: Update | null = null
|
||||||
let availableManualUpdate: ManualUpdate | null = null
|
let availableManualUpdate: ManualUpdate | null = null
|
||||||
@ -138,7 +138,7 @@ app.innerHTML = `
|
|||||||
<div>
|
<div>
|
||||||
<strong>System printer</strong>
|
<strong>System printer</strong>
|
||||||
<span id="virtual-printer-state" class="muted">Checking whether FieldLens Print is installed…</span>
|
<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>
|
||||||
<div class="actions printer-actions">
|
<div class="actions printer-actions">
|
||||||
<button class="btn" id="refresh-printers">Refresh printers</button>
|
<button class="btn" id="refresh-printers">Refresh printers</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user