fix(windows): forward captures through installed printer queue
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
d4631a04df
commit
3e17e1b55b
@ -30,11 +30,19 @@ original spool/document
|
|||||||
-> physical printer
|
-> physical printer
|
||||||
```
|
```
|
||||||
|
|
||||||
The current localhost:9100 RAW proxy remains a compatibility backend only until the native Windows backend is complete.
|
The current localhost:9100 capture listener remains a compatibility capture backend until the native Windows backend is complete. Forwarding from that listener should use the selected installed Windows printer queue through the Winspool RAW API by default, so USB, WSD, shared/local, and network printers work without requiring a destination TCP/9100 endpoint.
|
||||||
|
|
||||||
|
## Windows compatibility priority
|
||||||
|
|
||||||
|
1. Default forwarding to the selected installed Windows queue through Winspool (OpenPrinter / StartDocPrinter / WritePrinter) instead of connecting directly to the printer's TCP/9100 endpoint.
|
||||||
|
2. Validate USB, WSD, shared/local, IPP/class-driver, and network queues using the same-driver FieldLens virtual queue.
|
||||||
|
3. Keep direct RAW/TCP only as an advanced fallback.
|
||||||
|
4. Add Windows end-to-end capture/forward self-test and queue health diagnostics.
|
||||||
|
|
||||||
## Windows native backend implementation track
|
## Windows native backend implementation track
|
||||||
|
|
||||||
1. Spike the supported Windows spool interception options:
|
1. Build on the modern Windows Print Support Virtual Printer / Software Printer architecture rather than introducing a new legacy v3/v4 driver.
|
||||||
|
2. Spike the supported Windows spool interception options:
|
||||||
- custom port monitor,
|
- custom port monitor,
|
||||||
- print processor,
|
- print processor,
|
||||||
- virtual printer / print support backend.
|
- virtual printer / print support backend.
|
||||||
|
|||||||
@ -27,3 +27,7 @@ tauri-plugin-opener = "2"
|
|||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Graphics_Printing"] }
|
||||||
|
|||||||
@ -217,7 +217,12 @@ fn handle_raw_connection(
|
|||||||
) {
|
) {
|
||||||
let _ = source.set_read_timeout(Some(Duration::from_millis(750)));
|
let _ = source.set_read_timeout(Some(Duration::from_millis(750)));
|
||||||
let mut forward_error: Option<String> = None;
|
let mut forward_error: Option<String> = None;
|
||||||
let mut destination = if config.forward_addr.trim().is_empty() {
|
#[cfg(target_os = "windows")]
|
||||||
|
let prefer_system_printer = !config.forward_printer.trim().is_empty();
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let prefer_system_printer = false;
|
||||||
|
|
||||||
|
let mut destination = if prefer_system_printer || config.forward_addr.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
match config.forward_addr.parse() {
|
match config.forward_addr.parse() {
|
||||||
@ -267,9 +272,11 @@ fn handle_raw_connection(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tcp_requested = !config.forward_addr.trim().is_empty();
|
let tcp_requested = !prefer_system_printer && !config.forward_addr.trim().is_empty();
|
||||||
let (forwarded, final_error) = if forward_error.is_some() {
|
let (forwarded, final_error) = if forward_error.is_some() {
|
||||||
(false, forward_error)
|
(false, forward_error)
|
||||||
|
} else if prefer_system_printer {
|
||||||
|
forward_to_system_printer(&config.forward_printer, &bytes)
|
||||||
} else if tcp_requested {
|
} else if tcp_requested {
|
||||||
(true, None)
|
(true, None)
|
||||||
} else if !config.forward_printer.trim().is_empty() {
|
} else if !config.forward_printer.trim().is_empty() {
|
||||||
@ -817,12 +824,120 @@ fn forward_to_system_printer(printer: &str, bytes: &[u8]) -> (bool, Option<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(target_os = "windows")]
|
||||||
|
fn forward_to_system_printer(printer: &str, bytes: &[u8]) -> (bool, Option<String>) {
|
||||||
|
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr::{null, null_mut}};
|
||||||
|
use windows_sys::Win32::Graphics::Printing::{
|
||||||
|
ClosePrinter, EndDocPrinter, OpenPrinterW, StartDocPrinterW, WritePrinter,
|
||||||
|
DOC_INFO_1W, PRINTER_HANDLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
if printer.trim().is_empty() {
|
||||||
|
return (false, Some("no Windows destination printer selected".into()));
|
||||||
|
}
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return (false, Some("captured Windows print job was empty".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wide(value: &str) -> Vec<u16> {
|
||||||
|
OsStr::new(value).encode_wide().chain(std::iter::once(0)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
let printer_name = wide(printer);
|
||||||
|
let mut handle: PRINTER_HANDLE = null_mut();
|
||||||
|
|
||||||
|
let opened = unsafe { OpenPrinterW(printer_name.as_ptr(), &mut handle, null()) };
|
||||||
|
if opened == 0 || handle.is_null() {
|
||||||
|
return (
|
||||||
|
false,
|
||||||
|
Some(format!(
|
||||||
|
"Windows spooler could not open destination printer '{printer}': {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc_name = wide("FieldLens Print");
|
||||||
|
let raw = wide("RAW");
|
||||||
|
let doc = DOC_INFO_1W {
|
||||||
|
pDocName: doc_name.as_ptr() as *mut u16,
|
||||||
|
pOutputFile: null_mut(),
|
||||||
|
pDatatype: raw.as_ptr() as *mut u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
let job_id = unsafe {
|
||||||
|
StartDocPrinterW(
|
||||||
|
handle,
|
||||||
|
1,
|
||||||
|
(&doc as *const DOC_INFO_1W).cast::<u8>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if job_id == 0 {
|
||||||
|
let error = std::io::Error::last_os_error();
|
||||||
|
unsafe { ClosePrinter(handle); }
|
||||||
|
return (
|
||||||
|
false,
|
||||||
|
Some(format!(
|
||||||
|
"Windows spooler could not start RAW job on '{printer}': {error}"
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = 0usize;
|
||||||
|
let mut failure = None;
|
||||||
|
while offset < bytes.len() {
|
||||||
|
let remaining = &bytes[offset..];
|
||||||
|
let chunk_len = remaining.len().min(u32::MAX as usize);
|
||||||
|
let mut written = 0u32;
|
||||||
|
let ok = unsafe {
|
||||||
|
WritePrinter(
|
||||||
|
handle,
|
||||||
|
remaining.as_ptr().cast(),
|
||||||
|
chunk_len as u32,
|
||||||
|
&mut written,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 {
|
||||||
|
failure = Some(format!(
|
||||||
|
"Windows spooler write to '{printer}' failed after {offset} bytes: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if written == 0 {
|
||||||
|
failure = Some(format!(
|
||||||
|
"Windows spooler accepted zero bytes while forwarding to '{printer}'"
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += written as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ended = unsafe { EndDocPrinter(handle) };
|
||||||
|
let end_error = if ended == 0 {
|
||||||
|
Some(format!(
|
||||||
|
"Windows spooler could not finish RAW job on '{printer}': {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
unsafe { ClosePrinter(handle); }
|
||||||
|
|
||||||
|
if let Some(error) = failure.or(end_error) {
|
||||||
|
(false, Some(error))
|
||||||
|
} else {
|
||||||
|
(true, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||||
fn forward_to_system_printer(printer: &str, _bytes: &[u8]) -> (bool, Option<String>) {
|
fn forward_to_system_printer(printer: &str, _bytes: &[u8]) -> (bool, Option<String>) {
|
||||||
(
|
(
|
||||||
false,
|
false,
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"installed-printer RAW forwarding is not available for '{printer}' without a network RAW/TCP address"
|
"installed-printer RAW forwarding is not available for '{printer}' on this platform"
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/main.ts
21
src/main.ts
@ -149,7 +149,9 @@ let availableManualUpdate: ManualUpdate | null = null
|
|||||||
let updateBusy = false
|
let updateBusy = false
|
||||||
let captureError: string | null = null
|
let captureError: string | null = null
|
||||||
let systemPrinters: SystemPrinter[] = []
|
let systemPrinters: SystemPrinter[] = []
|
||||||
const isLinuxDesktop = navigator.userAgent.toLowerCase().includes('linux')
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
|
const isLinuxDesktop = userAgent.includes('linux')
|
||||||
|
const isWindowsDesktop = userAgent.includes('windows')
|
||||||
let virtualPrinterStatus: VirtualPrinterStatus = {
|
let virtualPrinterStatus: VirtualPrinterStatus = {
|
||||||
installed: false,
|
installed: false,
|
||||||
name: 'FieldLens Print',
|
name: 'FieldLens Print',
|
||||||
@ -460,7 +462,7 @@ function readForm(): AppConfig {
|
|||||||
...config,
|
...config,
|
||||||
mode: el<HTMLSelectElement>('mode').value as CaptureMode,
|
mode: el<HTMLSelectElement>('mode').value as CaptureMode,
|
||||||
listenAddr: el<HTMLInputElement>('listenAddr').value.trim(),
|
listenAddr: el<HTMLInputElement>('listenAddr').value.trim(),
|
||||||
forwardAddr: el<HTMLInputElement>('forwardAddr').value.trim(),
|
forwardAddr: isWindowsDesktop ? '' : el<HTMLInputElement>('forwardAddr').value.trim(),
|
||||||
forwardPrinter: el<HTMLSelectElement>('forwardPrinter').value,
|
forwardPrinter: el<HTMLSelectElement>('forwardPrinter').value,
|
||||||
spoolDir: el<HTMLInputElement>('spoolDir').value.trim(),
|
spoolDir: el<HTMLInputElement>('spoolDir').value.trim(),
|
||||||
apiBaseUrl: el<HTMLInputElement>('apiBaseUrl').value.trim(),
|
apiBaseUrl: el<HTMLInputElement>('apiBaseUrl').value.trim(),
|
||||||
@ -486,6 +488,11 @@ function writeForm(next: AppConfig) {
|
|||||||
el('capture-mode-field').setAttribute('hidden', '')
|
el('capture-mode-field').setAttribute('hidden', '')
|
||||||
el('listen-address-field').setAttribute('hidden', '')
|
el('listen-address-field').setAttribute('hidden', '')
|
||||||
el('raw-destination-field').setAttribute('hidden', '')
|
el('raw-destination-field').setAttribute('hidden', '')
|
||||||
|
} else if (isWindowsDesktop) {
|
||||||
|
el<HTMLSelectElement>('mode').value = 'raw-proxy'
|
||||||
|
el('capture-mode-field').setAttribute('hidden', '')
|
||||||
|
el('listen-address-field').setAttribute('hidden', '')
|
||||||
|
el('raw-destination-field').setAttribute('hidden', '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,10 +512,12 @@ function renderPrinters() {
|
|||||||
const selected = options.find((printer) => printer.name === select.value)
|
const selected = options.find((printer) => printer.name === select.value)
|
||||||
el('running-printer').textContent = selected?.name || config?.forwardPrinter || 'No physical printer selected'
|
el('running-printer').textContent = selected?.name || config?.forwardPrinter || 'No physical printer selected'
|
||||||
const help = el('forward-printer-help')
|
const help = el('forward-printer-help')
|
||||||
if (selected?.address) {
|
if (selected && isWindowsDesktop) {
|
||||||
|
help.textContent = 'FieldLens forwards through this installed Windows printer queue. USB, WSD, shared/local, and network ports do not require a separate RAW/TCP address.'
|
||||||
|
} else if (selected?.address) {
|
||||||
help.textContent = `Detected RAW network endpoint: ${selected.address}`
|
help.textContent = `Detected RAW network endpoint: ${selected.address}`
|
||||||
} else if (selected) {
|
} else if (selected) {
|
||||||
help.textContent = 'No RAW/TCP address was exposed for this queue. Enter the printer IP:9100 manually.'
|
help.textContent = 'No RAW/TCP address was exposed for this queue.'
|
||||||
} else if (config?.forwardPrinter && !options.some((printer) => printer.name === config.forwardPrinter)) {
|
} else if (config?.forwardPrinter && !options.some((printer) => printer.name === config.forwardPrinter)) {
|
||||||
help.textContent = `Previously selected printer "${config.forwardPrinter}" is not installed or available. Add it in system printer settings, then Refresh printers.`
|
help.textContent = `Previously selected printer "${config.forwardPrinter}" is not installed or available. Add it in system printer settings, then Refresh printers.`
|
||||||
} else if (!options.length) {
|
} else if (!options.length) {
|
||||||
@ -728,7 +737,9 @@ async function restartCapture() {
|
|||||||
|
|
||||||
el('forwardPrinter').addEventListener('change', () => {
|
el('forwardPrinter').addEventListener('change', () => {
|
||||||
const selected = systemPrinters.find((printer) => printer.name === el<HTMLSelectElement>('forwardPrinter').value)
|
const selected = systemPrinters.find((printer) => printer.name === el<HTMLSelectElement>('forwardPrinter').value)
|
||||||
if (selected?.address) {
|
if (isWindowsDesktop) {
|
||||||
|
el<HTMLInputElement>('forwardAddr').value = ''
|
||||||
|
} else if (selected?.address) {
|
||||||
el<HTMLInputElement>('forwardAddr').value = selected.address
|
el<HTMLInputElement>('forwardAddr').value = selected.address
|
||||||
} else if (selected) {
|
} else if (selected) {
|
||||||
el<HTMLInputElement>('forwardAddr').value = ''
|
el<HTMLInputElement>('forwardAddr').value = ''
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user