fix(linux): use CUPS cache for pre-driver capture handoff
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
b65204bffe
commit
a65b79e886
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fieldlens/print",
|
"name": "@fieldlens/print",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"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.10"
|
version = "0.1.11"
|
||||||
description = "FieldLens print-stream barcode capture companion"
|
description = "FieldLens print-stream barcode capture companion"
|
||||||
authors = ["Arrowstack"]
|
authors = ["Arrowstack"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@ -28,10 +28,6 @@ use crate::{
|
|||||||
const SYSTEM_CUPS_SPOOL_DIR: &str = "/var/cache/cups/fieldlens-print";
|
const SYSTEM_CUPS_SPOOL_DIR: &str = "/var/cache/cups/fieldlens-print";
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
const TEST_CAPTURE_SINK_ADDR: &str = "127.0.0.1:9101";
|
const TEST_CAPTURE_SINK_ADDR: &str = "127.0.0.1:9101";
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const CUPS_ORIGINAL_CAPTURE_ADDR: &str = "127.0.0.1:9102";
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const CUPS_TEST_CAPTURE_ADDR: &str = "127.0.0.1:9103";
|
|
||||||
|
|
||||||
pub struct CaptureRuntime {
|
pub struct CaptureRuntime {
|
||||||
stop: Arc<AtomicBool>,
|
stop: Arc<AtomicBool>,
|
||||||
@ -90,46 +86,22 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let system_spool = PathBuf::from(SYSTEM_CUPS_SPOOL_DIR);
|
let system_spool = PathBuf::from(SYSTEM_CUPS_SPOOL_DIR);
|
||||||
if system_spool.is_dir() && system_spool != PathBuf::from(&config.spool_dir) {
|
if system_spool != PathBuf::from(&config.spool_dir) {
|
||||||
let worker_stop = stop.clone();
|
let worker_stop = stop.clone();
|
||||||
let worker_state = state.clone();
|
let worker_state = state.clone();
|
||||||
let worker_app = app.clone();
|
let worker_app = app.clone();
|
||||||
let worker_config = config.clone();
|
let worker_config = config.clone();
|
||||||
handles.push(thread::spawn(move || {
|
handles.push(thread::spawn(move || {
|
||||||
spool_folder_loop(
|
cups_cache_folder_loop(
|
||||||
|
system_spool,
|
||||||
worker_config,
|
worker_config,
|
||||||
worker_state,
|
worker_state,
|
||||||
worker_app,
|
worker_app,
|
||||||
worker_stop,
|
worker_stop,
|
||||||
Some(system_spool),
|
|
||||||
"cups-original",
|
|
||||||
true,
|
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (address, source, forwarded) in [
|
|
||||||
(CUPS_ORIGINAL_CAPTURE_ADDR, "cups-original", true),
|
|
||||||
(CUPS_TEST_CAPTURE_ADDR, "cups-test", false),
|
|
||||||
] {
|
|
||||||
if let Ok(listener) = TcpListener::bind(address) {
|
|
||||||
let _ = listener.set_nonblocking(true);
|
|
||||||
let worker_stop = stop.clone();
|
|
||||||
let worker_state = state.clone();
|
|
||||||
let worker_app = app.clone();
|
|
||||||
handles.push(thread::spawn(move || {
|
|
||||||
cups_document_capture_loop(
|
|
||||||
listener,
|
|
||||||
worker_state,
|
|
||||||
worker_app,
|
|
||||||
worker_stop,
|
|
||||||
source,
|
|
||||||
forwarded,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sink = TcpListener::bind(TEST_CAPTURE_SINK_ADDR)
|
let sink = TcpListener::bind(TEST_CAPTURE_SINK_ADDR)
|
||||||
.with_context(|| format!("bind test-capture sink {TEST_CAPTURE_SINK_ADDR}"))?;
|
.with_context(|| format!("bind test-capture sink {TEST_CAPTURE_SINK_ADDR}"))?;
|
||||||
sink.set_nonblocking(true)?;
|
sink.set_nonblocking(true)?;
|
||||||
@ -144,91 +116,6 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
Ok(CaptureRuntime { stop, handles })
|
Ok(CaptureRuntime { stop, handles })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn cups_document_capture_loop(
|
|
||||||
listener: TcpListener,
|
|
||||||
state: SharedState,
|
|
||||||
app: AppHandle,
|
|
||||||
stop: Arc<AtomicBool>,
|
|
||||||
source: &'static str,
|
|
||||||
forwarded: bool,
|
|
||||||
) {
|
|
||||||
while !stop.load(Ordering::SeqCst) {
|
|
||||||
match listener.accept() {
|
|
||||||
Ok((stream, _)) => {
|
|
||||||
let worker_state = state.clone();
|
|
||||||
let worker_app = app.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
handle_cups_document_connection(
|
|
||||||
stream,
|
|
||||||
worker_state,
|
|
||||||
worker_app,
|
|
||||||
source,
|
|
||||||
forwarded,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 handle_cups_document_connection(
|
|
||||||
mut stream: TcpStream,
|
|
||||||
state: SharedState,
|
|
||||||
app: AppHandle,
|
|
||||||
source: &'static str,
|
|
||||||
forwarded: bool,
|
|
||||||
) {
|
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(4)));
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
let mut buffer = [0u8; 32 * 1024];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match stream.read(&mut buffer) {
|
|
||||||
Ok(0) => break,
|
|
||||||
Ok(read) => bytes.extend_from_slice(&buffer[..read]),
|
|
||||||
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {
|
|
||||||
if !bytes.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
process_payload(
|
|
||||||
&state,
|
|
||||||
&app,
|
|
||||||
source,
|
|
||||||
"cups-live-capture".into(),
|
|
||||||
bytes,
|
|
||||||
forwarded,
|
|
||||||
Some(format!("CUPS live capture read failed: {error}")),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process_payload(
|
|
||||||
&state,
|
|
||||||
&app,
|
|
||||||
source,
|
|
||||||
format!("cups-live-{}.pdf", Uuid::new_v4()),
|
|
||||||
bytes,
|
|
||||||
forwarded,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn test_capture_sink_loop(listener: TcpListener, stop: Arc<AtomicBool>) {
|
fn test_capture_sink_loop(listener: TcpListener, stop: Arc<AtomicBool>) {
|
||||||
while !stop.load(Ordering::SeqCst) {
|
while !stop.load(Ordering::SeqCst) {
|
||||||
@ -378,6 +265,33 @@ fn handle_raw_connection(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn cups_cache_folder_loop(
|
||||||
|
spool_dir: PathBuf,
|
||||||
|
config: AppConfig,
|
||||||
|
state: SharedState,
|
||||||
|
app: AppHandle,
|
||||||
|
stop: Arc<AtomicBool>,
|
||||||
|
) {
|
||||||
|
while !stop.load(Ordering::SeqCst) && !spool_dir.is_dir() {
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
if stop.load(Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spool_folder_loop(
|
||||||
|
config,
|
||||||
|
state,
|
||||||
|
app,
|
||||||
|
stop,
|
||||||
|
Some(spool_dir),
|
||||||
|
"cups-original",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn spool_folder_loop(
|
fn spool_folder_loop(
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
|
|||||||
@ -25,9 +25,7 @@ const FIELDLENS_CUPS_SPOOL_DIR: &str = "/var/cache/cups/fieldlens-print";
|
|||||||
const FIELDLENS_CUPS_FILTER: &str = r#"#!/usr/bin/python3
|
const FIELDLENS_CUPS_FILTER: &str = r#"#!/usr/bin/python3
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
|
||||||
|
|
||||||
SPOOL = os.path.join(os.environ.get("CUPS_CACHEDIR", "/var/cache/cups"), "fieldlens-print")
|
SPOOL = os.path.join(os.environ.get("CUPS_CACHEDIR", "/var/cache/cups"), "fieldlens-print")
|
||||||
|
|
||||||
@ -39,60 +37,45 @@ job_id = safe(sys.argv[1] if len(sys.argv) > 1 else "job")
|
|||||||
title = safe(sys.argv[3] if len(sys.argv) > 3 else "print")
|
title = safe(sys.argv[3] if len(sys.argv) > 3 else "print")
|
||||||
printer = safe(os.environ.get("PRINTER", "printer"))
|
printer = safe(os.environ.get("PRINTER", "printer"))
|
||||||
content_type = os.environ.get("CONTENT_TYPE", "application/pdf")
|
content_type = os.environ.get("CONTENT_TYPE", "application/pdf")
|
||||||
ext = {
|
is_test = printer.lower() == "fieldlens-test-capture"
|
||||||
"application/pdf": "pdf",
|
prefix = "test-" if is_test else "print-"
|
||||||
"application/postscript": "ps",
|
ext = "pdf" if "pdf" in content_type else "dat"
|
||||||
"image/png": "png",
|
|
||||||
"image/jpeg": "jpg",
|
# Use a deterministic name so multiple PDF pre-filter stages for the same
|
||||||
}.get(content_type, "dat")
|
# CUPS job do not create duplicate FieldLens jobs.
|
||||||
prefix = "test-" if printer.lower() == "fieldlens-test-capture" else "print-"
|
name = f"{prefix}{printer}-{job_id}-{title}.{ext}"
|
||||||
name = f"{prefix}{printer}-{job_id}-{title}-{uuid.uuid4().hex}.{ext}"
|
|
||||||
final_path = os.path.join(SPOOL, name)
|
final_path = os.path.join(SPOOL, name)
|
||||||
part_path = final_path + ".part"
|
part_path = final_path + ".part"
|
||||||
|
|
||||||
source = None
|
source = None
|
||||||
live = None
|
|
||||||
capture = None
|
capture = None
|
||||||
captured_ok = False
|
captured_ok = False
|
||||||
live_ok = False
|
|
||||||
try:
|
try:
|
||||||
if len(sys.argv) > 6 and sys.argv[6]:
|
if len(sys.argv) > 6 and sys.argv[6]:
|
||||||
source = open(sys.argv[6], "rb")
|
source = open(sys.argv[6], "rb")
|
||||||
else:
|
else:
|
||||||
source = sys.stdin.buffer
|
source = sys.stdin.buffer
|
||||||
|
|
||||||
live_port = 9103 if printer.lower() == "fieldlens-test-capture" else 9102
|
# If an earlier pre-filter stage already captured this job, only pass the
|
||||||
try:
|
# bytes onward. Otherwise tee the pre-driver document into CUPS_CACHEDIR.
|
||||||
live = socket.create_connection(("127.0.0.1", live_port), timeout=0.35)
|
if not os.path.exists(final_path):
|
||||||
live.settimeout(None)
|
try:
|
||||||
except OSError:
|
os.makedirs(SPOOL, exist_ok=True)
|
||||||
live = None
|
capture = open(part_path, "wb")
|
||||||
|
except OSError as error:
|
||||||
try:
|
print(f"WARNING: FieldLens capture cache unavailable: {error}", file=sys.stderr)
|
||||||
os.makedirs(SPOOL, exist_ok=True)
|
capture = None
|
||||||
capture = open(part_path, "wb")
|
|
||||||
except OSError:
|
|
||||||
capture = None
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
chunk = source.read(65536)
|
chunk = source.read(65536)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
|
|
||||||
if live is not None:
|
|
||||||
try:
|
|
||||||
live.sendall(chunk)
|
|
||||||
except OSError:
|
|
||||||
try:
|
|
||||||
live.close()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
live = None
|
|
||||||
|
|
||||||
if capture is not None:
|
if capture is not None:
|
||||||
try:
|
try:
|
||||||
capture.write(chunk)
|
capture.write(chunk)
|
||||||
except OSError:
|
except OSError as error:
|
||||||
|
print(f"WARNING: FieldLens capture write failed: {error}", file=sys.stderr)
|
||||||
try:
|
try:
|
||||||
capture.close()
|
capture.close()
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -103,39 +86,20 @@ try:
|
|||||||
|
|
||||||
sys.stdout.buffer.flush()
|
sys.stdout.buffer.flush()
|
||||||
|
|
||||||
if live is not None:
|
|
||||||
try:
|
|
||||||
live.shutdown(socket.SHUT_WR)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
live.close()
|
|
||||||
live = None
|
|
||||||
live_ok = True
|
|
||||||
|
|
||||||
if capture is not None:
|
if capture is not None:
|
||||||
capture.flush()
|
capture.flush()
|
||||||
capture.close()
|
capture.close()
|
||||||
capture = None
|
capture = None
|
||||||
if live_ok:
|
os.chmod(part_path, 0o644)
|
||||||
try:
|
os.replace(part_path, final_path)
|
||||||
os.unlink(part_path)
|
captured_ok = True
|
||||||
except OSError:
|
print(f"INFO: FieldLens captured {name}", file=sys.stderr)
|
||||||
pass
|
|
||||||
else:
|
|
||||||
os.chmod(part_path, 0o644)
|
|
||||||
os.replace(part_path, final_path)
|
|
||||||
captured_ok = True
|
|
||||||
finally:
|
finally:
|
||||||
if source is not None and source is not sys.stdin.buffer:
|
if source is not None and source is not sys.stdin.buffer:
|
||||||
source.close()
|
source.close()
|
||||||
if live is not None:
|
|
||||||
try:
|
|
||||||
live.close()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if capture is not None:
|
if capture is not None:
|
||||||
capture.close()
|
capture.close()
|
||||||
if not live_ok and not captured_ok:
|
if not captured_ok:
|
||||||
try:
|
try:
|
||||||
os.unlink(part_path)
|
os.unlink(part_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -620,7 +584,8 @@ fn build_fieldlens_ppd(target_printer: &str) -> Result<PathBuf> {
|
|||||||
|
|
||||||
let prefilter = concat!(
|
let prefilter = concat!(
|
||||||
"*% FieldLens original-document capture before printer rendering\n",
|
"*% FieldLens original-document capture before printer rendering\n",
|
||||||
"*cupsPreFilter: \"application/pdf 0 fieldlens-capture\"\n"
|
"*cupsPreFilter: \"application/pdf 0 fieldlens-capture\"\n",
|
||||||
|
"*cupsPreFilter: \"application/vnd.cups-pdf 0 fieldlens-capture\"\n"
|
||||||
);
|
);
|
||||||
if let Some(index) = cleaned.rfind("*% End") {
|
if let Some(index) = cleaned.rfind("*% End") {
|
||||||
cleaned.insert_str(index, prefilter);
|
cleaned.insert_str(index, prefilter);
|
||||||
|
|||||||
@ -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.10",
|
"version": "0.1.11",
|
||||||
"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.10'
|
let currentVersion = '0.1.11'
|
||||||
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user