use std::process::Command; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; #[cfg(any(target_os = "windows", target_os = "linux"))] use std::fs; #[cfg(target_os = "linux")] use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; #[cfg(target_os = "windows")] 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 = "windows")] const CREATE_NO_WINDOW: u32 = 0x08000000; #[cfg(target_os = "windows")] fn hidden_windows_command(program: &str) -> Command { let mut command = Command::new(program); command.creation_flags(CREATE_NO_WINDOW); command } #[cfg(target_os = "linux")] const FIELDLENS_CUPS_SPOOL_DIR: &str = "/var/lib/fieldlens-print/capture"; #[cfg(target_os = "linux")] const FIELDLENS_CUPS_FILTER: &str = r#"#!/usr/bin/python3 import os import re import sys CACHE = "/var/lib/fieldlens-print/capture" OWNER_UID = __FIELDLENS_OWNER_UID__ OWNER_GID = __FIELDLENS_OWNER_GID__ def safe(value): value = re.sub(r"[^A-Za-z0-9._-]+", "-", value or "job").strip("-") return value[:80] or "job" 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") printer = safe(os.environ.get("PRINTER", "printer")) prefix = "test-" if printer.lower() == "fieldlens-test-capture" else "print-" suffix = "" if title.lower().endswith(".pdf") else ".pdf" name = f"{prefix}{printer}-{job_id}-{title}{suffix}" final_path = os.path.join(CACHE, name) part_path = final_path + ".part" source = None capture = None captured_ok = False try: if len(sys.argv) > 6 and sys.argv[6]: source = open(sys.argv[6], "rb") else: source = sys.stdin.buffer try: os.makedirs(CACHE, exist_ok=True) if not os.path.exists(final_path): capture = open(part_path, "wb") except OSError as error: print(f"WARNING: FieldLens capture cache unavailable: {error}", file=sys.stderr) capture = None while True: chunk = source.read(65536) if not chunk: break if capture is not None: try: capture.write(chunk) except OSError as error: print(f"WARNING: FieldLens capture write failed: {error}", file=sys.stderr) try: capture.close() except OSError: pass capture = None sys.stdout.buffer.write(chunk) sys.stdout.buffer.flush() if capture is not None: capture.flush() capture.close() capture = None os.chmod(part_path, 0o600) try: os.chown(part_path, OWNER_UID, OWNER_GID) except PermissionError: # On this Linux lane CUPS runs filters as root. Keep printing alive # if a future CUPS policy drops privileges; repair can surface it. pass os.replace(part_path, final_path) captured_ok = True print(f"INFO: FieldLens captured {name}", file=sys.stderr) finally: if source is not None and source is not sys.stdin.buffer: source.close() if capture is not None: capture.close() if not captured_ok: try: os.unlink(part_path) except OSError: 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")] const FIELDLENS_CUPS_REPAIR_HELPER: &str = r#"#!/usr/bin/python3 import os import subprocess import sys import tempfile target_printer = sys.argv[1] target_port = sys.argv[2] temp_filter = sys.argv[3] filter_path = sys.argv[4] owner_uid = int(sys.argv[5]) owner_gid = int(sys.argv[6]) source_ppd = os.path.join("/etc/cups/ppd", f"{target_printer}.ppd") spool_dir = "/var/lib/fieldlens-print/capture" old_spool_dir = "/var/cache/cups/fieldlens-print" with open(source_ppd, "r", encoding="utf-8", errors="replace") as source: original = source.read() cleaned = "\n".join( line for line in original.splitlines() if "fieldlens-capture" not in line ) + "\n" prefilter = ( "*% FieldLens original-document capture before cups-filters universal conversion\n" "*cupsPreFilter: \"application/vnd.universal-input 0 fieldlens-capture\"\n" ) index = cleaned.rfind("*% End") if index >= 0: cleaned = cleaned[:index] + prefilter + cleaned[index:] else: cleaned += prefilter fd, ppd_path = tempfile.mkstemp(prefix="fieldlens-", suffix=".ppd") os.close(fd) try: with open(ppd_path, "w", encoding="utf-8") as target: target.write(cleaned) subprocess.run(["install", "-m", "0755", temp_filter, filter_path], check=True) os.makedirs(spool_dir, exist_ok=True) os.chown(spool_dir, owner_uid, owner_gid) os.chmod(spool_dir, 0o700) # Bring forward captures stranded by 0.1.13/0.1.14 in CUPS' protected # cache. Ignore directories and partial writes. if os.path.isdir(old_spool_dir): for name in os.listdir(old_spool_dir): source = os.path.join(old_spool_dir, name) if not os.path.isfile(source) or name.endswith(".part"): continue target = os.path.join(spool_dir, name) if os.path.exists(target): root, ext = os.path.splitext(name) target = os.path.join(spool_dir, f"{root}-migrated{ext}") try: os.replace(source, target) except OSError: continue try: os.chown(target, owner_uid, owner_gid) os.chmod(target, 0o600) except OSError: pass subprocess.run([ "lpadmin", "-p", "FieldLens-Print", "-E", "-v", target_port, "-P", ppd_path, "-D", "FieldLens Print", ], check=True) subprocess.run([ "lpadmin", "-p", "FieldLens-Test-Capture", "-E", "-v", "socket://127.0.0.1:9101", "-P", ppd_path, "-D", "FieldLens Test Capture", ], check=True) for queue in ("FieldLens-Print", "FieldLens-Test-Capture"): subprocess.run(["cupsenable", queue], check=True) subprocess.run(["cupsaccept", queue], check=True) finally: try: os.unlink(ppd_path) except OSError: pass "#; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SystemPrinter { pub name: String, pub driver: String, pub port: String, pub address: Option, pub is_virtual: bool, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct VirtualPrinterStatus { pub installed: bool, pub name: String, pub detail: String, } fn is_virtual_printer_name(name: &str) -> bool { 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> { #[cfg(target_os = "windows")] { return list_windows_printers(); } #[cfg(target_os = "linux")] { return list_linux_printers(); } #[allow(unreachable_code)] Ok(Vec::new()) } pub fn virtual_printer_status() -> Result { #[cfg(target_os = "linux")] { return linux_virtual_printer_status(); } let installed = list_system_printers()? .iter() .any(|printer| is_virtual_printer_name(&printer.name)); Ok(VirtualPrinterStatus { installed, name: VIRTUAL_PRINTER_NAME.into(), detail: if installed { "Ready in the system print dialog. Keep RAW capture running while printing.".into() } else { "Not installed yet. Choose the physical destination printer, then create it.".into() }, }) } pub fn install_virtual_printer(target_printer: &str) -> Result { let target_printer = target_printer.trim(); if target_printer.is_empty() { return Err(anyhow!("choose the physical printer to forward to first")); } if is_virtual_printer_name(target_printer) { return Err(anyhow!("FieldLens Print cannot forward to itself")); } #[cfg(target_os = "windows")] install_windows_virtual_printer(target_printer)?; #[cfg(target_os = "linux")] install_linux_virtual_printer(target_printer)?; virtual_printer_status() } pub fn remove_virtual_printer() -> Result { #[cfg(target_os = "windows")] remove_windows_virtual_printer()?; #[cfg(target_os = "linux")] remove_linux_virtual_printer()?; virtual_printer_status() } #[cfg(target_os = "windows")] fn list_windows_printers() -> Result> { let script = r#" $items = @( Get-Printer | ForEach-Object { $printer = $_ $port = Get-PrinterPort -Name $printer.PortName -ErrorAction SilentlyContinue $address = $null if ($null -ne $port -and $port.PrinterHostAddress) { $number = if ($port.PortNumber) { $port.PortNumber } else { 9100 } $address = "$($port.PrinterHostAddress):$number" } [pscustomobject]@{ name = $printer.Name driver = $printer.DriverName port = $printer.PortName address = $address isVirtual = ($printer.Name -eq 'FieldLens Print') } } ) $items | ConvertTo-Json -Compress "#; let output = hidden_windows_command("powershell.exe") .args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script]) .output() .context("query Windows printers")?; if !output.status.success() { return Err(anyhow!( "Windows printer query failed: {}", String::from_utf8_lossy(&output.stderr).trim() )); } let stdout = String::from_utf8_lossy(&output.stdout); let value: Value = serde_json::from_str(stdout.trim()).context("decode Windows printer list")?; let values = match value { Value::Array(values) => values, Value::Null => vec![], value => vec![value], }; let mut printers = Vec::new(); for value in values { let printer: SystemPrinter = serde_json::from_value(value)?; printers.push(printer); } printers.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); Ok(printers) } #[cfg(target_os = "windows")] fn install_windows_virtual_printer(target_printer: &str) -> Result<()> { let target = ps_quote(target_printer); let script_path = std::env::temp_dir().join(format!( "fieldlens-printer-setup-{}.ps1", uuid::Uuid::new_v4() )); let script = format!( r#"param([string]$TargetPrinter) $ErrorActionPreference = 'Stop' $target = Get-Printer -Name $TargetPrinter -ErrorAction Stop $portName = '{port}' $queueName = '{queue}' if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {{ Add-PrinterPort -Name $portName -PrinterHostAddress '127.0.0.1' -PortNumber 9100 }} $targetConfig = Get-PrintConfiguration -PrinterName $TargetPrinter -ErrorAction SilentlyContinue $existing = Get-Printer -Name $queueName -ErrorAction SilentlyContinue if ($existing) {{ Set-Printer -Name $queueName -DriverName $target.DriverName -PortName $portName }} else {{ Add-Printer -Name $queueName -DriverName $target.DriverName -PortName $portName }} # The virtual queue uses the same driver as the physical destination, so its # PrintTicket can be copied safely. This keeps paper size, duplex settings and # especially orientation aligned with the real printer instead of falling back # to the driver's portrait default. if ($null -ne $targetConfig -and $targetConfig.PrintTicketXML) {{ Set-PrintConfiguration -PrinterName $queueName -PrintTicketXml $targetConfig.PrintTicketXML }} "#, port = WINDOWS_PORT_NAME, queue = VIRTUAL_PRINTER_NAME, ); fs::write(&script_path, script).context("write Windows printer setup script")?; // First run the script directly. This is the reliable path when FieldLens // itself was started as administrator and avoids a second hidden RunAs // process plus fragile nested PowerShell argument quoting. let direct = hidden_windows_command("powershell.exe") .args([ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", ]) .arg(&script_path) .args(["-TargetPrinter", target_printer]) .output() .context("run Windows printer setup")?; if direct.status.success() { let _ = fs::remove_file(&script_path); return Ok(()); } // If the app is not elevated, retry through UAC. Keep this as a fallback // rather than the primary path so admin-launched builds never depend on a // nested hidden RunAs process. let path = ps_quote(&script_path.to_string_lossy()); let elevated = format!( "$p = Start-Process -FilePath powershell.exe -Verb RunAs -Wait -PassThru -ArgumentList @('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File','{path}','-TargetPrinter','{target}'); exit $p.ExitCode" ); let elevated_status = hidden_windows_command("powershell.exe") .args(["-NoProfile", "-NonInteractive", "-Command", &elevated]) .status() .context("launch elevated Windows printer setup")?; let _ = fs::remove_file(&script_path); if !elevated_status.success() { let stderr = String::from_utf8_lossy(&direct.stderr).trim().to_string(); return Err(anyhow!( "Windows printer setup failed{}", if stderr.is_empty() { "; administrator approval may be required".to_string() } else { format!(": {stderr}") } )); } Ok(()) } #[cfg(target_os = "windows")] fn remove_windows_virtual_printer() -> Result<()> { let script = format!( r#"$ErrorActionPreference = 'Stop' $queueName = '{queue}' $portName = '{port}' if (Get-Printer -Name $queueName -ErrorAction SilentlyContinue) {{ Remove-Printer -Name $queueName }} if (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue) {{ Remove-PrinterPort -Name $portName }} "#, queue = VIRTUAL_PRINTER_NAME, port = WINDOWS_PORT_NAME, ); let script_path = std::env::temp_dir().join(format!( "fieldlens-printer-remove-{}.ps1", uuid::Uuid::new_v4() )); fs::write(&script_path, script)?; let direct = hidden_windows_command("powershell.exe") .args([ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", ]) .arg(&script_path) .output() .context("run Windows printer removal")?; if direct.status.success() { let _ = fs::remove_file(&script_path); return Ok(()); } let path = ps_quote(&script_path.to_string_lossy()); let elevated = format!( "$p = Start-Process -FilePath powershell.exe -Verb RunAs -Wait -PassThru -ArgumentList @('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File','{path}'); exit $p.ExitCode" ); let status = hidden_windows_command("powershell.exe") .args(["-NoProfile", "-NonInteractive", "-Command", &elevated]) .status()?; let _ = fs::remove_file(&script_path); if !status.success() { let stderr = String::from_utf8_lossy(&direct.stderr).trim().to_owned(); return Err(anyhow!( "removing FieldLens Print failed{}", if stderr.is_empty() { String::new() } else { format!(": {stderr}") } )); } Ok(()) } #[cfg(target_os = "windows")] fn ps_quote(value: &str) -> String { value.replace('\'', "''") } #[cfg(target_os = "linux")] fn list_linux_printers() -> Result> { let output = match Command::new("lpstat").arg("-v").output() { Ok(output) => output, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { return Err(anyhow!( "CUPS client tools are missing. Install the cups-client package, then reopen FieldLens Print." )) } Err(error) => return Err(error).context("query CUPS printers with lpstat"), }; if !output.status.success() { return Err(anyhow!( "CUPS printer query failed: {}", String::from_utf8_lossy(&output.stderr).trim() )); } let mut printers = Vec::new(); for line in String::from_utf8_lossy(&output.stdout).lines() { let Some(rest) = line.strip_prefix("device for ") else { continue; }; let Some((name, uri)) = rest.split_once(": ") else { continue; }; // Ignore placeholder/dummy queues. They can expose a PPD but are not a // usable physical destination and can make Chromium reject the cloned // FieldLens queue as "not installed correctly". if uri == "///dev/null" || uri == "file:///dev/null" { continue; } let address = socket_uri_to_address(uri); printers.push(SystemPrinter { name: name.to_string(), driver: String::new(), port: uri.to_string(), address, is_virtual: is_virtual_printer_name(name), }); } printers.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); Ok(printers) } #[cfg(target_os = "linux")] fn socket_uri_to_address(uri: &str) -> Option { let value = uri.strip_prefix("socket://")?; let authority = value.split('/').next().unwrap_or(value); if authority.contains(':') { Some(authority.to_string()) } else if authority.is_empty() { None } else { Some(format!("{authority}:9100")) } } #[cfg(target_os = "linux")] fn linux_queue_ready(queue: &str) -> Result { let state = Command::new("lpstat") .args(["-p", queue]) .output() .with_context(|| format!("query {queue} CUPS state"))?; let accepting = Command::new("lpstat") .args(["-a", queue]) .output() .with_context(|| format!("query {queue} accepting state"))?; let state_text = String::from_utf8_lossy(&state.stdout); let accepting_text = String::from_utf8_lossy(&accepting.stdout); Ok( state.status.success() && !state_text.to_ascii_lowercase().contains("disabled") && accepting.status.success() && 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 { 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 { installed: true, name: VIRTUAL_PRINTER_NAME.into(), 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(), }) } #[cfg(target_os = "linux")] fn run_cups_repair_command(program: &str, args: &[&str]) -> Result<()> { let direct = Command::new(program).args(args).status(); if matches!(direct, Ok(status) if status.success()) { return Ok(()); } let status = Command::new("pkexec") .arg(program) .args(args) .status() .with_context(|| format!("run elevated {program}"))?; if !status.success() { return Err(anyhow!("{program} failed; administrator approval may be required")); } Ok(()) } #[cfg(target_os = "linux")] fn cups_filter_dir() -> Result<&'static Path> { for candidate in ["/usr/lib/cups/filter", "/usr/libexec/cups/filter"] { let path = Path::new(candidate); if path.is_dir() { return Ok(path); } } Err(anyhow!("CUPS filter directory was not found")) } #[cfg(target_os = "linux")] fn run_linux_admin(program: &str, args: &[String], context: &str) -> Result<()> { let direct = Command::new(program).args(args).status(); if matches!(direct, Ok(status) if status.success()) { return Ok(()); } let status = Command::new("pkexec") .arg(program) .args(args) .status() .with_context(|| format!("run elevated {context}"))?; if !status.success() { return Err(anyhow!("{context} failed; administrator approval may be required")); } Ok(()) } #[cfg(target_os = "linux")] fn install_linux_capture_filter() -> Result<()> { let filter_dir = cups_filter_dir()?; let filter_path = filter_dir.join("fieldlens-capture"); let temp_filter = std::env::temp_dir().join(format!( "fieldlens-capture-{}.py", uuid::Uuid::new_v4() )); fs::write(&temp_filter, FIELDLENS_CUPS_FILTER).context("write FieldLens CUPS pre-filter")?; let install_filter_args = vec![ "-m".to_string(), "0755".to_string(), temp_filter.to_string_lossy().into_owned(), filter_path.to_string_lossy().into_owned(), ]; let filter_result = run_linux_admin( "install", &install_filter_args, "install FieldLens CUPS pre-filter", ); let _ = fs::remove_file(&temp_filter); filter_result?; let spool_args = vec![ "-d".to_string(), "-m".to_string(), "0777".to_string(), FIELDLENS_CUPS_SPOOL_DIR.to_string(), ]; run_linux_admin("install", &spool_args, "create FieldLens CUPS capture spool")?; Ok(()) } #[cfg(target_os = "linux")] fn build_fieldlens_ppd(target_printer: &str) -> Result { let source = PathBuf::from("/etc/cups/ppd").join(format!("{target_printer}.ppd")); if !source.exists() { return Err(anyhow!( "CUPS PPD for '{target_printer}' was not found; FieldLens needs the installed printer definition to preserve browser print capabilities" )); } let original = match fs::read_to_string(&source) { Ok(contents) => contents, Err(direct_error) => { // CUPS commonly stores PPDs as root:lp 0640. Desktop users can // enumerate the queue but cannot necessarily read its PPD. // Reuse the existing PolicyKit setup flow for a read-only fallback. let output = Command::new("pkexec") .arg("cat") .arg(&source) .output() .with_context(|| { format!( "read target printer PPD {} (direct read failed: {direct_error})", source.display() ) })?; if !output.status.success() { return Err(anyhow!( "could not read target printer PPD {}: {}", source.display(), String::from_utf8_lossy(&output.stderr).trim() )); } String::from_utf8(output.stdout) .with_context(|| format!("decode target printer PPD {}", source.display()))? } }; let mut cleaned = original .lines() .filter(|line| !line.contains("fieldlens-capture")) .collect::>() .join("\n"); cleaned.push('\n'); let prefilter = concat!( "*% FieldLens original-document capture before cups-filters universal conversion\n", "*cupsPreFilter: \"application/vnd.universal-input 0 fieldlens-capture\"\n" ); if let Some(index) = cleaned.rfind("*% End") { cleaned.insert_str(index, prefilter); } else { cleaned.push_str(prefilter); } let target = std::env::temp_dir().join(format!( "fieldlens-{}.ppd", uuid::Uuid::new_v4() )); fs::write(&target, cleaned).context("write FieldLens derived PPD")?; Ok(target) } #[cfg(target_os = "linux")] fn install_linux_virtual_printer(target_printer: &str) -> Result<()> { let printers = list_linux_printers()?; let target = printers .iter() .find(|printer| printer.name == target_printer && !printer.is_virtual) .ok_or_else(|| anyhow!( "selected printer '{target_printer}' is not installed or available in CUPS" ))?; let filter_dir = cups_filter_dir()?; let filter_path = filter_dir.join("fieldlens-capture"); let temp_filter = std::env::temp_dir().join(format!( "fieldlens-capture-{}.py", uuid::Uuid::new_v4() )); let temp_helper = std::env::temp_dir().join(format!( "fieldlens-cups-repair-{}.py", uuid::Uuid::new_v4() )); let uid = Command::new("id") .arg("-u") .output() .context("read current Linux user id")?; let gid = Command::new("id") .arg("-g") .output() .context("read current Linux group id")?; if !uid.status.success() || !gid.status.success() { return Err(anyhow!("could not determine the current Linux user for capture ownership")); } let uid = String::from_utf8_lossy(&uid.stdout).trim().to_string(); let gid = String::from_utf8_lossy(&gid.stdout).trim().to_string(); let filter_script = render_linux_capture_filter(&uid, &gid); fs::write(&temp_filter, filter_script) .context("write FieldLens CUPS capture filter")?; fs::write(&temp_helper, FIELDLENS_CUPS_REPAIR_HELPER) .context("write FieldLens CUPS repair helper")?; let status = Command::new("pkexec") .arg("python3") .arg(&temp_helper) .arg(target_printer) .arg(&target.port) .arg(&temp_filter) .arg(&filter_path) .arg(&uid) .arg(&gid) .status() .context("run elevated FieldLens CUPS repair")?; let _ = fs::remove_file(&temp_filter); let _ = fs::remove_file(&temp_helper); if !status.success() { return Err(anyhow!( "FieldLens printer repair failed; administrator approval may be required" )); } Ok(()) } #[cfg(target_os = "linux")] fn remove_linux_virtual_printer() -> Result<()> { 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") .arg("lpadmin") .args(&args) .status() .context("run elevated CUPS printer removal")?; if !status.success() { return Err(anyhow!("removing FieldLens CUPS printer '{queue}' failed")); } } 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) ); } }