fix(linux): make CUPS original-document capture authoritative
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
09d817f748
commit
cbaae90161
@ -11,6 +11,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
|
|||||||
libxdo-dev \
|
libxdo-dev \
|
||||||
patchelf \
|
patchelf \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
python3 \
|
||||||
librsvg2-dev \
|
librsvg2-dev \
|
||||||
wget \
|
wget \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@ -50,9 +50,13 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
let stop = Arc::new(AtomicBool::new(false));
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
if matches!(config.mode, CaptureMode::RawProxy | CaptureMode::Both) {
|
if matches!(config.mode, CaptureMode::RawProxy | CaptureMode::Both) {
|
||||||
|
// Compatibility backend. This boundary is intentionally Windows-only
|
||||||
|
// so a native spool/document capture backend can replace localhost:9100
|
||||||
|
// without changing downstream job processing.
|
||||||
let listener = TcpListener::bind(&config.listen_addr)
|
let listener = TcpListener::bind(&config.listen_addr)
|
||||||
.with_context(|| format!("bind RAW capture listener {}", config.listen_addr))?;
|
.with_context(|| format!("bind Windows RAW compatibility listener {}", config.listen_addr))?;
|
||||||
listener.set_nonblocking(true)?;
|
listener.set_nonblocking(true)?;
|
||||||
|
|
||||||
let worker_stop = stop.clone();
|
let worker_stop = stop.clone();
|
||||||
@ -98,6 +102,9 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
{
|
{
|
||||||
for spool in [SYSTEM_CUPS_SPOOL_DIR, LEGACY_CUPS_SPOOL_DIR] {
|
for spool in [SYSTEM_CUPS_SPOOL_DIR, LEGACY_CUPS_SPOOL_DIR] {
|
||||||
let system_spool = PathBuf::from(spool);
|
let system_spool = PathBuf::from(spool);
|
||||||
|
if spool == LEGACY_CUPS_SPOOL_DIR && fs::read_dir(&system_spool).is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
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();
|
||||||
@ -113,16 +120,17 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let sink = TcpListener::bind(TEST_CAPTURE_SINK_ADDR)
|
// The self-test queue uses the same CUPS pre-filter as production.
|
||||||
.with_context(|| format!("bind test-capture sink {TEST_CAPTURE_SINK_ADDR}"))?;
|
// Port 9101 only consumes the rendered output so no paper is produced.
|
||||||
|
// Failure to bind this optional sink must not stop production capture.
|
||||||
|
if let Ok(sink) = TcpListener::bind(TEST_CAPTURE_SINK_ADDR) {
|
||||||
sink.set_nonblocking(true)?;
|
sink.set_nonblocking(true)?;
|
||||||
let worker_stop = stop.clone();
|
let worker_stop = stop.clone();
|
||||||
let worker_state = state.clone();
|
|
||||||
let worker_app = app.clone();
|
|
||||||
handles.push(thread::spawn(move || {
|
handles.push(thread::spawn(move || {
|
||||||
test_capture_sink_loop(sink, worker_state, worker_app, worker_stop)
|
test_capture_sink_loop(sink, worker_stop)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
return Err(anyhow!("no capture route enabled"));
|
return Err(anyhow!("no capture route enabled"));
|
||||||
@ -132,18 +140,11 @@ pub fn start(config: AppConfig, state: SharedState, app: AppHandle) -> Result<Ca
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn test_capture_sink_loop(
|
fn test_capture_sink_loop(listener: TcpListener, stop: Arc<AtomicBool>) {
|
||||||
listener: TcpListener,
|
|
||||||
state: SharedState,
|
|
||||||
app: AppHandle,
|
|
||||||
stop: Arc<AtomicBool>,
|
|
||||||
) {
|
|
||||||
while !stop.load(Ordering::SeqCst) {
|
while !stop.load(Ordering::SeqCst) {
|
||||||
match listener.accept() {
|
match listener.accept() {
|
||||||
Ok((stream, _)) => {
|
Ok((stream, _)) => {
|
||||||
let state = state.clone();
|
thread::spawn(move || drain_test_capture_stream(stream));
|
||||||
let app = app.clone();
|
|
||||||
thread::spawn(move || handle_test_capture_stream(stream, state, app));
|
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == ErrorKind::WouldBlock => {
|
Err(error) if error.kind() == ErrorKind::WouldBlock => {
|
||||||
thread::sleep(Duration::from_millis(80));
|
thread::sleep(Duration::from_millis(80));
|
||||||
@ -156,46 +157,16 @@ fn test_capture_sink_loop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn read_test_capture_stream<R: Read>(mut reader: R) -> std::io::Result<Vec<u8>> {
|
fn drain_test_capture_stream(mut stream: TcpStream) {
|
||||||
let mut bytes = Vec::new();
|
|
||||||
let mut buffer = [0u8; 32 * 1024];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match reader.read(&mut buffer) {
|
|
||||||
Ok(0) => break,
|
|
||||||
Ok(read) => bytes.extend_from_slice(&buffer[..read]),
|
|
||||||
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => break,
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn handle_test_capture_stream(mut stream: TcpStream, state: SharedState, app: AppHandle) {
|
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
|
||||||
|
let mut buffer = [0u8; 32 * 1024];
|
||||||
match read_test_capture_stream(&mut stream) {
|
loop {
|
||||||
Ok(bytes) if !bytes.is_empty() => process_payload(
|
match stream.read(&mut buffer) {
|
||||||
&state,
|
Ok(0) => break,
|
||||||
&app,
|
|
||||||
"cups-test",
|
|
||||||
"FieldLens-Test-Capture".into(),
|
|
||||||
bytes,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(error) => process_payload(
|
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => break,
|
||||||
&state,
|
Err(_) => break,
|
||||||
&app,
|
}
|
||||||
"cups-test",
|
|
||||||
"FieldLens-Test-Capture".into(),
|
|
||||||
vec![],
|
|
||||||
false,
|
|
||||||
Some(format!("test capture read failed: {error}")),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -909,25 +880,3 @@ fn process_payload(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod self_test_regressions {
|
|
||||||
use super::*;
|
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn self_test_stream_payload_is_preserved_for_capture_processing() {
|
|
||||||
let payload = b"%PDF-1.4\nFieldLens capture self-test\n%%EOF\n";
|
|
||||||
let bytes = read_test_capture_stream(Cursor::new(payload)).expect("read self-test payload");
|
|
||||||
assert_eq!(bytes, payload);
|
|
||||||
assert!(!bytes.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_self_test_stream_stays_empty() {
|
|
||||||
let bytes = read_test_capture_stream(Cursor::new(Vec::<u8>::new()))
|
|
||||||
.expect("read empty self-test payload");
|
|
||||||
assert!(bytes.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ mod zpl;
|
|||||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{fs, path::{Path, PathBuf}, time::SystemTime};
|
use std::{fs, path::{Path, PathBuf}, time::{Duration, Instant, SystemTime}};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
use tauri::{AppHandle, Manager, State};
|
use tauri::{AppHandle, Manager, State};
|
||||||
@ -264,17 +264,19 @@ fn get_debug_snapshot(state: State<'_, AppState>) -> Result<DebugSnapshot, Strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn run_capture_self_test() -> Result<String, String> {
|
fn run_capture_self_test(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let path = std::env::temp_dir().join(format!(
|
if state.runtime.lock().is_none() {
|
||||||
"fieldlens-capture-self-test-{}.pdf",
|
return Err("Capture is not running. Start capture before running the self-test.".into());
|
||||||
uuid::Uuid::new_v4()
|
}
|
||||||
));
|
|
||||||
|
let token = format!("fieldlens-self-test-{}", uuid::Uuid::new_v4());
|
||||||
|
let path = std::env::temp_dir().join(format!("{token}.pdf"));
|
||||||
std::fs::write(&path, build_self_test_pdf()).map_err(|error| error.to_string())?;
|
std::fs::write(&path, build_self_test_pdf()).map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
let output = std::process::Command::new("lp")
|
let output = std::process::Command::new("lp")
|
||||||
.args(["-d", "FieldLens-Test-Capture"])
|
.args(["-d", "FieldLens-Test-Capture", "-t", &token])
|
||||||
.arg(&path)
|
.arg(&path)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|error| error.to_string());
|
.map_err(|error| error.to_string());
|
||||||
@ -288,16 +290,40 @@ fn run_capture_self_test() -> Result<String, String> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let detail = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let deadline = Instant::now() + Duration::from_secs(8);
|
||||||
return Ok(if detail.is_empty() {
|
loop {
|
||||||
"Self-test submitted to FieldLens Test Capture.".into()
|
if let Some(job) = state
|
||||||
} else {
|
.shared
|
||||||
detail
|
.jobs
|
||||||
});
|
.lock()
|
||||||
|
.iter()
|
||||||
|
.find(|job| job.source == "cups-test" && job.label.contains(&token))
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
if let Some(error) = job.error {
|
||||||
|
return Err(format!("Self-test capture failed: {error}"));
|
||||||
|
}
|
||||||
|
return Ok(format!(
|
||||||
|
"Self-test captured successfully ({} bytes).",
|
||||||
|
job.bytes
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(
|
||||||
|
"CUPS accepted the self-test job, but FieldLens did not observe the corresponding original-document capture within 8 seconds. Repair the FieldLens queues/filter and retry."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
{
|
{
|
||||||
|
let _ = state;
|
||||||
Err("Capture self-test is currently available on Linux only.".into())
|
Err("Capture self-test is currently available on Linux only.".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,12 @@ pub struct AppConfig {
|
|||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
mode: CaptureMode::RawProxy,
|
mode: {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{ CaptureMode::SpoolFolder }
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{ CaptureMode::RawProxy }
|
||||||
|
},
|
||||||
listen_addr: "127.0.0.1:9100".into(),
|
listen_addr: "127.0.0.1:9100".into(),
|
||||||
forward_addr: String::new(),
|
forward_addr: String::new(),
|
||||||
forward_printer: String::new(),
|
forward_printer: String::new(),
|
||||||
|
|||||||
@ -39,7 +39,9 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
CACHE = "/var/lib/fieldlens-print/capture"\nOWNER_UID = __FIELDLENS_OWNER_UID__\nOWNER_GID = __FIELDLENS_OWNER_GID__
|
CACHE = "/var/lib/fieldlens-print/capture"
|
||||||
|
OWNER_UID = __FIELDLENS_OWNER_UID__
|
||||||
|
OWNER_GID = __FIELDLENS_OWNER_GID__
|
||||||
|
|
||||||
def safe(value):
|
def safe(value):
|
||||||
value = re.sub(r"[^A-Za-z0-9._-]+", "-", value or "job").strip("-")
|
value = re.sub(r"[^A-Za-z0-9._-]+", "-", value or "job").strip("-")
|
||||||
@ -118,6 +120,13 @@ finally:
|
|||||||
pass
|
pass
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn render_linux_capture_filter(uid: &str, gid: &str) -> String {
|
||||||
|
FIELDLENS_CUPS_FILTER
|
||||||
|
.replace("__FIELDLENS_OWNER_UID__", uid)
|
||||||
|
.replace("__FIELDLENS_OWNER_GID__", gid)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
const FIELDLENS_CUPS_REPAIR_HELPER: &str = r#"#!/usr/bin/python3
|
const FIELDLENS_CUPS_REPAIR_HELPER: &str = r#"#!/usr/bin/python3
|
||||||
import os
|
import os
|
||||||
@ -586,45 +595,90 @@ fn socket_uri_to_address(uri: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn linux_virtual_printer_status() -> Result<VirtualPrinterStatus> {
|
fn linux_queue_ready(queue: &str) -> Result<bool> {
|
||||||
let printers = list_linux_printers()?;
|
|
||||||
let installed = printers
|
|
||||||
.iter()
|
|
||||||
.any(|printer| printer.name.eq_ignore_ascii_case("FieldLens-Print"));
|
|
||||||
|
|
||||||
if !installed {
|
|
||||||
return Ok(VirtualPrinterStatus {
|
|
||||||
installed: false,
|
|
||||||
name: VIRTUAL_PRINTER_NAME.into(),
|
|
||||||
detail: "Not installed yet. Choose the physical destination printer, then create it.".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = Command::new("lpstat")
|
let state = Command::new("lpstat")
|
||||||
.args(["-p", "FieldLens-Print"])
|
.args(["-p", queue])
|
||||||
.output()
|
.output()
|
||||||
.context("query FieldLens Print CUPS state")?;
|
.with_context(|| format!("query {queue} CUPS state"))?;
|
||||||
let accepting = Command::new("lpstat")
|
let accepting = Command::new("lpstat")
|
||||||
.args(["-a", "FieldLens-Print"])
|
.args(["-a", queue])
|
||||||
.output()
|
.output()
|
||||||
.context("query FieldLens Print accepting state")?;
|
.with_context(|| format!("query {queue} accepting state"))?;
|
||||||
|
|
||||||
let state_text = String::from_utf8_lossy(&state.stdout);
|
let state_text = String::from_utf8_lossy(&state.stdout);
|
||||||
let accepting_text = String::from_utf8_lossy(&accepting.stdout);
|
let accepting_text = String::from_utf8_lossy(&accepting.stdout);
|
||||||
let enabled = state.status.success()
|
Ok(
|
||||||
&& !state_text.to_ascii_lowercase().contains("disabled");
|
state.status.success()
|
||||||
let accepts_jobs = accepting.status.success()
|
&& !state_text.to_ascii_lowercase().contains("disabled")
|
||||||
&& accepting_text.to_ascii_lowercase().contains("accepting requests");
|
&& accepting.status.success()
|
||||||
let ready = enabled && accepts_jobs;
|
&& accepting_text.to_ascii_lowercase().contains("accepting requests"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn linux_capture_filter_health() -> Result<()> {
|
||||||
|
let filter_path = cups_filter_dir()?.join("fieldlens-capture");
|
||||||
|
let metadata = fs::metadata(&filter_path)
|
||||||
|
.with_context(|| format!("FieldLens CUPS filter missing at {}", filter_path.display()))?;
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(anyhow!("FieldLens CUPS filter is not a regular file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let check = Command::new("python3")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("import pathlib,sys; p=sys.argv[1]; compile(pathlib.Path(p).read_text(), p, 'exec')")
|
||||||
|
.arg(&filter_path)
|
||||||
|
.output()
|
||||||
|
.context("validate FieldLens CUPS filter with python3")?;
|
||||||
|
if !check.status.success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"FieldLens CUPS filter is invalid: {}",
|
||||||
|
String::from_utf8_lossy(&check.stderr).trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn linux_virtual_printer_status() -> Result<VirtualPrinterStatus> {
|
||||||
|
let printers = list_linux_printers()?;
|
||||||
|
let print_installed = printers
|
||||||
|
.iter()
|
||||||
|
.any(|printer| printer.name.eq_ignore_ascii_case("FieldLens-Print"));
|
||||||
|
let test_installed = printers
|
||||||
|
.iter()
|
||||||
|
.any(|printer| printer.name.eq_ignore_ascii_case(TEST_VIRTUAL_PRINTER_QUEUE));
|
||||||
|
|
||||||
|
if !print_installed || !test_installed {
|
||||||
|
return Ok(VirtualPrinterStatus {
|
||||||
|
installed: false,
|
||||||
|
name: VIRTUAL_PRINTER_NAME.into(),
|
||||||
|
detail: "FieldLens CUPS queues are incomplete. Click Create / repair printers.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let print_ready = linux_queue_ready("FieldLens-Print")?;
|
||||||
|
let test_ready = linux_queue_ready(TEST_VIRTUAL_PRINTER_QUEUE)?;
|
||||||
|
if !print_ready || !test_ready {
|
||||||
|
return Ok(VirtualPrinterStatus {
|
||||||
|
installed: false,
|
||||||
|
name: VIRTUAL_PRINTER_NAME.into(),
|
||||||
|
detail: "A FieldLens CUPS queue is disabled or not accepting jobs. Click Create / repair printers.".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = linux_capture_filter_health() {
|
||||||
|
return Ok(VirtualPrinterStatus {
|
||||||
|
installed: false,
|
||||||
|
name: VIRTUAL_PRINTER_NAME.into(),
|
||||||
|
detail: format!("Capture filter is broken: {error}. Click Create / repair printers."),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(VirtualPrinterStatus {
|
Ok(VirtualPrinterStatus {
|
||||||
installed: ready,
|
installed: true,
|
||||||
name: VIRTUAL_PRINTER_NAME.into(),
|
name: VIRTUAL_PRINTER_NAME.into(),
|
||||||
detail: if ready {
|
detail: "Ready. Original documents are captured by the CUPS pre-filter before normal CUPS rendering; test jobs use the same capture path with rendered output discarded.".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()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,9 +862,7 @@ fn install_linux_virtual_printer(target_printer: &str) -> Result<()> {
|
|||||||
let uid = String::from_utf8_lossy(&uid.stdout).trim().to_string();
|
let uid = String::from_utf8_lossy(&uid.stdout).trim().to_string();
|
||||||
let gid = String::from_utf8_lossy(&gid.stdout).trim().to_string();
|
let gid = String::from_utf8_lossy(&gid.stdout).trim().to_string();
|
||||||
|
|
||||||
let filter_script = FIELDLENS_CUPS_FILTER
|
let filter_script = render_linux_capture_filter(&uid, &gid);
|
||||||
.replace("__FIELDLENS_OWNER_UID__", &uid)
|
|
||||||
.replace("__FIELDLENS_OWNER_GID__", &gid);
|
|
||||||
fs::write(&temp_filter, filter_script)
|
fs::write(&temp_filter, filter_script)
|
||||||
.context("write FieldLens CUPS capture filter")?;
|
.context("write FieldLens CUPS capture filter")?;
|
||||||
fs::write(&temp_helper, FIELDLENS_CUPS_REPAIR_HELPER)
|
fs::write(&temp_helper, FIELDLENS_CUPS_REPAIR_HELPER)
|
||||||
@ -860,3 +912,32 @@ fn remove_linux_virtual_printer() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(all(test, target_os = "linux"))]
|
||||||
|
mod linux_filter_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cups_filter_compiles_with_python() {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"fieldlens-filter-test-{}.py",
|
||||||
|
uuid::Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let script = render_linux_capture_filter("1000", "1000");
|
||||||
|
fs::write(&path, script).expect("write rendered CUPS filter");
|
||||||
|
|
||||||
|
let output = Command::new("python3")
|
||||||
|
.args(["-m", "py_compile"])
|
||||||
|
.arg(&path)
|
||||||
|
.output()
|
||||||
|
.expect("run python3 -m py_compile");
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"rendered CUPS filter must compile: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -37,6 +37,11 @@ impl SharedState {
|
|||||||
fs::create_dir_all(&data_dir).context("create FieldLens Print data directory")?;
|
fs::create_dir_all(&data_dir).context("create FieldLens Print data directory")?;
|
||||||
|
|
||||||
let mut config = read_json::<AppConfig>(&data_dir.join("config.json")).unwrap_or_default();
|
let mut config = read_json::<AppConfig>(&data_dir.join("config.json")).unwrap_or_default();
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
config.mode = crate::model::CaptureMode::SpoolFolder;
|
||||||
|
config.forward_addr.clear();
|
||||||
|
}
|
||||||
if config.spool_dir.trim().is_empty() {
|
if config.spool_dir.trim().is_empty() {
|
||||||
config.spool_dir = data_dir.join("spool").to_string_lossy().into_owned();
|
config.spool_dir = data_dir.join("spool").to_string_lossy().into_owned();
|
||||||
}
|
}
|
||||||
@ -105,7 +110,12 @@ impl SharedState {
|
|||||||
self.persist_auth()
|
self.persist_auth()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_config(&self, next: AppConfig) -> Result<AppConfig> {
|
pub fn replace_config(&self, mut next: AppConfig) -> Result<AppConfig> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
next.mode = crate::model::CaptureMode::SpoolFolder;
|
||||||
|
next.forward_addr.clear();
|
||||||
|
}
|
||||||
fs::create_dir_all(&next.spool_dir).ok();
|
fs::create_dir_all(&next.spool_dir).ok();
|
||||||
*self.config.lock() = next.clone();
|
*self.config.lock() = next.clone();
|
||||||
self.persist_config()?;
|
self.persist_config()?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user