feat: remote printer (#11231)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2025-03-27 15:34:27 +08:00
committed by GitHub
parent 1cb53c1f7a
commit f4bbf82363
101 changed files with 3707 additions and 211 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "remote_printer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[target.'cfg(target_os = "windows")'.dependencies]
hbb_common = { version = "0.1.0", path = "../hbb_common" }
winapi = { version = "0.3" }
windows-strings = "0.3.1"

View File

@@ -0,0 +1,34 @@
#[cfg(target_os = "windows")]
mod setup;
#[cfg(target_os = "windows")]
pub use setup::{
is_rd_printer_installed,
setup::{install_update_printer, uninstall_printer},
};
#[cfg(target_os = "windows")]
const RD_DRIVER_INF_PATH: &str = "drivers/RustDeskPrinterDriver/RustDeskPrinterDriver.inf";
#[cfg(target_os = "windows")]
fn get_printer_name(app_name: &str) -> Vec<u16> {
format!("{} Printer", app_name)
.encode_utf16()
.chain(Some(0))
.collect()
}
#[cfg(target_os = "windows")]
fn get_driver_name() -> Vec<u16> {
"RustDesk v4 Printer Driver"
.encode_utf16()
.chain(Some(0))
.collect()
}
#[cfg(target_os = "windows")]
fn get_port_name(app_name: &str) -> Vec<u16> {
format!("{} Printer", app_name)
.encode_utf16()
.chain(Some(0))
.collect()
}

View File

@@ -0,0 +1,202 @@
use super::{common_enum, get_wstr_bytes, is_name_equal};
use hbb_common::{bail, log, ResultType};
use std::{io, ptr::null_mut, time::Duration};
use winapi::{
shared::{
minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD, MAX_PATH},
ntdef::{DWORDLONG, LPCWSTR},
winerror::{ERROR_UNKNOWN_PRINTER_DRIVER, S_OK},
},
um::{
winspool::{
DeletePrinterDriverExW, DeletePrinterDriverPackageW, EnumPrinterDriversW,
InstallPrinterDriverFromPackageW, UploadPrinterDriverPackageW, DPD_DELETE_ALL_FILES,
DRIVER_INFO_6W, DRIVER_INFO_8W, IPDFP_COPY_ALL_FILES, UPDP_SILENT_UPLOAD,
UPDP_UPLOAD_ALWAYS,
},
winuser::GetForegroundWindow,
},
};
use windows_strings::PCWSTR;
const HRESULT_ERR_ELEMENT_NOT_FOUND: u32 = 0x80070490;
fn enum_printer_driver(
level: DWORD,
p_driver_info: LPBYTE,
cb_buf: DWORD,
pcb_needed: LPDWORD,
pc_returned: LPDWORD,
) -> BOOL {
unsafe {
// https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers
// This is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors
// such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
EnumPrinterDriversW(
null_mut(),
null_mut(),
level,
p_driver_info,
cb_buf,
pcb_needed,
pc_returned,
)
}
}
pub fn get_installed_driver_version(name: &PCWSTR) -> ResultType<Option<DWORDLONG>> {
common_enum(
"EnumPrinterDriversW",
enum_printer_driver,
6,
|info: &DRIVER_INFO_6W| {
if is_name_equal(name, info.pName) {
Some(info.dwlDriverVersion)
} else {
None
}
},
|| None,
)
}
fn find_inf(name: &PCWSTR) -> ResultType<Vec<u16>> {
let r = common_enum(
"EnumPrinterDriversW",
enum_printer_driver,
8,
|info: &DRIVER_INFO_8W| {
if is_name_equal(name, info.pName) {
Some(get_wstr_bytes(info.pszInfPath))
} else {
None
}
},
|| None,
)?;
Ok(r.unwrap_or(vec![]))
}
fn delete_printer_driver(name: &PCWSTR) -> ResultType<()> {
unsafe {
// If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer.
// `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9).
// We can only ignore this error for now.
// Though restarting the spooler service is a solution, it's not a good idea to restart the service.
//
// Deleting the printer driver after deleting the printer is a common practice.
// No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once.
// https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422
// AnyDesk printer driver and the simplest printer driver also have the same issue.
if FALSE
== DeletePrinterDriverExW(
null_mut(),
null_mut(),
name.as_ptr() as _,
DPD_DELETE_ALL_FILES,
0,
)
{
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(ERROR_UNKNOWN_PRINTER_DRIVER as _) {
return Ok(());
} else {
bail!("Failed to delete the printer driver, {}", err)
}
}
}
Ok(())
}
// https://github.com/dvalter/chromium-android-ext-dev/blob/dab74f7d5bc5a8adf303090ee25c611b4d54e2db/cloud_print/virtual_driver/win/install/setup.cc#L190
fn delete_printer_driver_package(inf: Vec<u16>) -> ResultType<()> {
if inf.is_empty() {
return Ok(());
}
let slen = if inf[inf.len() - 1] == 0 {
inf.len() - 1
} else {
inf.len()
};
let inf_path = String::from_utf16_lossy(&inf[..slen]);
if !std::path::Path::new(&inf_path).exists() {
return Ok(());
}
let mut retries = 3;
loop {
unsafe {
let res = DeletePrinterDriverPackageW(null_mut(), inf.as_ptr(), null_mut());
if res == S_OK || res == HRESULT_ERR_ELEMENT_NOT_FOUND as i32 {
return Ok(());
}
log::error!("Failed to delete the printer driver, result: {}", res);
}
retries -= 1;
if retries <= 0 {
bail!("Failed to delete the printer driver");
}
std::thread::sleep(Duration::from_secs(2));
}
}
pub fn uninstall_driver(name: &PCWSTR) -> ResultType<()> {
// Note: inf must be found before `delete_printer_driver()`.
let inf = find_inf(name)?;
delete_printer_driver(name)?;
delete_printer_driver_package(inf)
}
pub fn install_driver(name: &PCWSTR, inf: LPCWSTR) -> ResultType<()> {
let mut size = (MAX_PATH * 10) as u32;
let mut package_path = [0u16; MAX_PATH * 10];
unsafe {
let mut res = UploadPrinterDriverPackageW(
null_mut(),
inf,
null_mut(),
UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS,
null_mut(),
package_path.as_mut_ptr(),
&mut size as _,
);
if res != S_OK {
log::error!(
"Failed to upload the printer driver package to the driver cache silently, {}. Will try with user UI.",
res
);
res = UploadPrinterDriverPackageW(
null_mut(),
inf,
null_mut(),
UPDP_UPLOAD_ALWAYS,
GetForegroundWindow(),
package_path.as_mut_ptr(),
&mut size as _,
);
if res != S_OK {
bail!(
"Failed to upload the printer driver package to the driver cache with UI, {}",
res
);
}
}
// https://learn.microsoft.com/en-us/windows/win32/printdocs/installprinterdriverfrompackage
res = InstallPrinterDriverFromPackageW(
null_mut(),
package_path.as_ptr(),
name.as_ptr(),
null_mut(),
IPDFP_COPY_ALL_FILES,
);
if res != S_OK {
bail!("Failed to install the printer driver from package, {}", res);
}
}
Ok(())
}

View File

@@ -0,0 +1,99 @@
use hbb_common::{bail, ResultType};
use std::{io, ptr::null_mut};
use winapi::{
shared::{
minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD},
ntdef::{LPCWSTR, LPWSTR},
},
um::winbase::{lstrcmpiW, lstrlenW},
};
use windows_strings::PCWSTR;
mod driver;
mod port;
pub(crate) mod printer;
pub(crate) mod setup;
#[inline]
pub fn is_rd_printer_installed(app_name: &str) -> ResultType<bool> {
let printer_name = crate::get_printer_name(app_name);
let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr());
printer::is_printer_added(&rd_printer_name)
}
fn get_wstr_bytes(p: LPWSTR) -> Vec<u16> {
let mut vec_bytes = vec![];
unsafe {
let len: isize = lstrlenW(p) as _;
if len > 0 {
for i in 0..len + 1 {
vec_bytes.push(*p.offset(i));
}
}
}
vec_bytes
}
fn is_name_equal(name: &PCWSTR, name_from_api: LPCWSTR) -> bool {
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw
// For some locales, the lstrcmpi function may be insufficient.
// If this occurs, use `CompareStringEx` to ensure proper comparison.
// For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison.
// Note that specifying these values slows performance, so use them only when necessary.
//
// No need to consider `CompareStringEx` for now.
unsafe { lstrcmpiW(name.as_ptr(), name_from_api) == 0 }
}
fn common_enum<T, R: Sized>(
enum_name: &str,
enum_fn: fn(
Level: DWORD,
pDriverInfo: LPBYTE,
cbBuf: DWORD,
pcbNeeded: LPDWORD,
pcReturned: LPDWORD,
) -> BOOL,
level: DWORD,
on_data: impl Fn(&T) -> Option<R>,
on_no_data: impl Fn() -> Option<R>,
) -> ResultType<Option<R>> {
let mut needed = 0;
let mut returned = 0;
enum_fn(level, null_mut(), 0, &mut needed, &mut returned);
if needed == 0 {
return Ok(on_no_data());
}
let mut buffer = vec![0u8; needed as usize];
if FALSE
== enum_fn(
level,
buffer.as_mut_ptr(),
needed,
&mut needed,
&mut returned,
)
{
bail!(
"Failed to call {}, error: {}",
enum_name,
io::Error::last_os_error()
)
}
// to-do: how to free the buffers in *const T?
let p_enum_info = buffer.as_ptr() as *const T;
unsafe {
for i in 0..returned {
let enum_info = p_enum_info.offset(i as isize);
let r = on_data(&*enum_info);
if r.is_some() {
return Ok(r);
}
}
}
Ok(on_no_data())
}

View File

@@ -0,0 +1,128 @@
use super::{common_enum, is_name_equal, printer::get_printer_installed_on_port};
use hbb_common::{bail, ResultType};
use std::{io, ptr::null_mut};
use winapi::{
shared::minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD},
um::{
winnt::HANDLE,
winspool::{
ClosePrinter, EnumPortsW, OpenPrinterW, XcvDataW, PORT_INFO_2W, PRINTER_DEFAULTSW,
SERVER_WRITE,
},
},
};
use windows_strings::{w, PCWSTR};
const XCV_MONITOR_LOCAL_PORT: PCWSTR = w!(",XcvMonitor Local Port");
fn enum_printer_port(
level: DWORD,
p_port_info: LPBYTE,
cb_buf: DWORD,
pcb_needed: LPDWORD,
pc_returned: LPDWORD,
) -> BOOL {
unsafe {
// https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports
// This is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors
// such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
EnumPortsW(
null_mut(),
level,
p_port_info,
cb_buf,
pcb_needed,
pc_returned,
)
}
}
fn is_port_exists(name: &PCWSTR) -> ResultType<bool> {
let r = common_enum(
"EnumPortsW",
enum_printer_port,
2,
|info: &PORT_INFO_2W| {
if is_name_equal(name, info.pPortName) {
Some(true)
} else {
None
}
},
|| None,
)?;
Ok(r.unwrap_or(false))
}
unsafe fn execute_on_local_port(port: &PCWSTR, command: &PCWSTR) -> ResultType<()> {
let mut dft = PRINTER_DEFAULTSW {
pDataType: null_mut(),
pDevMode: null_mut(),
DesiredAccess: SERVER_WRITE,
};
let mut h_monitor: HANDLE = null_mut();
if FALSE
== OpenPrinterW(
XCV_MONITOR_LOCAL_PORT.as_ptr() as _,
&mut h_monitor,
&mut dft as *mut PRINTER_DEFAULTSW as _,
)
{
bail!(format!(
"Failed to open Local Port monitor. Error: {}",
io::Error::last_os_error()
))
}
let mut output_needed: u32 = 0;
let mut status: u32 = 0;
if FALSE
== XcvDataW(
h_monitor,
command.as_ptr(),
port.as_ptr() as *mut u8,
(port.len() + 1) as u32 * 2,
null_mut(),
0,
&mut output_needed,
&mut status,
)
{
ClosePrinter(h_monitor);
bail!(format!(
"Failed to execute the command on the printer port, Error: {}",
io::Error::last_os_error()
))
}
ClosePrinter(h_monitor);
Ok(())
}
fn add_local_port(port: &PCWSTR) -> ResultType<()> {
unsafe { execute_on_local_port(port, &w!("AddPort")) }
}
fn delete_local_port(port: &PCWSTR) -> ResultType<()> {
unsafe { execute_on_local_port(port, &w!("DeletePort")) }
}
pub fn check_add_local_port(port: &PCWSTR) -> ResultType<()> {
if !is_port_exists(port)? {
return add_local_port(port);
}
Ok(())
}
pub fn check_delete_local_port(port: &PCWSTR) -> ResultType<()> {
if is_port_exists(port)? {
if get_printer_installed_on_port(port)?.is_some() {
bail!("The printer is installed on the port. Please remove the printer first.");
}
return delete_local_port(port);
}
Ok(())
}

View File

@@ -0,0 +1,161 @@
use super::{common_enum, get_wstr_bytes, is_name_equal};
use hbb_common::{bail, ResultType};
use std::{io, ptr::null_mut};
use winapi::{
shared::{
minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD},
ntdef::HANDLE,
winerror::ERROR_INVALID_PRINTER_NAME,
},
um::winspool::{
AddPrinterW, ClosePrinter, DeletePrinter, EnumPrintersW, OpenPrinterW, SetPrinterW,
PRINTER_ALL_ACCESS, PRINTER_ATTRIBUTE_LOCAL, PRINTER_CONTROL_PURGE, PRINTER_DEFAULTSW,
PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_2W,
},
};
use windows_strings::{w, PCWSTR};
fn enum_local_printer(
level: DWORD,
p_printer_info: LPBYTE,
cb_buf: DWORD,
pcb_needed: LPDWORD,
pc_returned: LPDWORD,
) -> BOOL {
unsafe {
// https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters
// This is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors
// such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
EnumPrintersW(
PRINTER_ENUM_LOCAL,
null_mut(),
level,
p_printer_info,
cb_buf,
pcb_needed,
pc_returned,
)
}
}
#[inline]
pub fn is_printer_added(name: &PCWSTR) -> ResultType<bool> {
let r = common_enum(
"EnumPrintersW",
enum_local_printer,
1,
|info: &PRINTER_INFO_1W| {
if is_name_equal(name, info.pName) {
Some(true)
} else {
None
}
},
|| None,
)?;
Ok(r.unwrap_or(false))
}
// Only return the first matched printer
pub fn get_printer_installed_on_port(port: &PCWSTR) -> ResultType<Option<Vec<u16>>> {
common_enum(
"EnumPrintersW",
enum_local_printer,
2,
|info: &PRINTER_INFO_2W| {
if is_name_equal(port, info.pPortName) {
Some(get_wstr_bytes(info.pPrinterName))
} else {
None
}
},
|| None,
)
}
pub fn add_printer(name: &PCWSTR, driver: &PCWSTR, port: &PCWSTR) -> ResultType<()> {
let mut printer_info = PRINTER_INFO_2W {
pServerName: null_mut(),
pPrinterName: name.as_ptr() as _,
pShareName: null_mut(),
pPortName: port.as_ptr() as _,
pDriverName: driver.as_ptr() as _,
pComment: null_mut(),
pLocation: null_mut(),
pDevMode: null_mut(),
pSepFile: null_mut(),
pPrintProcessor: w!("WinPrint").as_ptr() as _,
pDatatype: w!("RAW").as_ptr() as _,
pParameters: null_mut(),
pSecurityDescriptor: null_mut(),
Attributes: PRINTER_ATTRIBUTE_LOCAL,
Priority: 0,
DefaultPriority: 0,
StartTime: 0,
UntilTime: 0,
Status: 0,
cJobs: 0,
AveragePPM: 0,
};
unsafe {
let h_printer = AddPrinterW(
null_mut(),
2,
&mut printer_info as *mut PRINTER_INFO_2W as _,
);
if h_printer.is_null() {
bail!(format!(
"Failed to add printer. Error: {}",
io::Error::last_os_error()
))
}
}
Ok(())
}
pub fn delete_printer(name: &PCWSTR) -> ResultType<()> {
let mut dft = PRINTER_DEFAULTSW {
pDataType: null_mut(),
pDevMode: null_mut(),
DesiredAccess: PRINTER_ALL_ACCESS,
};
let mut h_printer: HANDLE = null_mut();
unsafe {
if FALSE
== OpenPrinterW(
name.as_ptr() as _,
&mut h_printer,
&mut dft as *mut PRINTER_DEFAULTSW as _,
)
{
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(ERROR_INVALID_PRINTER_NAME as _) {
return Ok(());
} else {
bail!(format!("Failed to open printer. Error: {}", err))
}
}
if FALSE == SetPrinterW(h_printer, 0, null_mut(), PRINTER_CONTROL_PURGE) {
ClosePrinter(h_printer);
bail!(format!(
"Failed to purge printer queue. Error: {}",
io::Error::last_os_error()
))
}
if FALSE == DeletePrinter(h_printer) {
ClosePrinter(h_printer);
bail!(format!(
"Failed to delete printer. Error: {}",
io::Error::last_os_error()
))
}
ClosePrinter(h_printer);
}
Ok(())
}

View File

@@ -0,0 +1,94 @@
use super::{
driver::{get_installed_driver_version, install_driver, uninstall_driver},
port::{check_add_local_port, check_delete_local_port},
printer::{add_printer, delete_printer},
};
use hbb_common::{allow_err, bail, lazy_static, log, ResultType};
use std::{path::PathBuf, sync::Mutex};
use windows_strings::PCWSTR;
lazy_static::lazy_static!(
static ref SETUP_MTX: Mutex<()> = Mutex::new(());
);
fn get_driver_inf_abs_path() -> ResultType<PathBuf> {
use crate::RD_DRIVER_INF_PATH;
let exe_file = std::env::current_exe()?;
let abs_path = match exe_file.parent() {
Some(parent) => parent.join(RD_DRIVER_INF_PATH),
None => bail!(
"Invalid exe parent for {}",
exe_file.to_string_lossy().as_ref()
),
};
if !abs_path.exists() {
bail!(
"The driver inf file \"{}\" does not exists",
RD_DRIVER_INF_PATH
)
}
Ok(abs_path)
}
// Note: This function must be called in a separate thread.
// Because many functions in this module are blocking or synchronous.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
// Steps:
// 1. Add the local port.
// 2. Check if the driver is installed.
// Uninstall the existing driver if it is installed.
// We should not check the driver version because the driver is deployed with the application.
// It's better to uninstall the existing driver and install the driver from the application.
// 3. Add the printer.
pub fn install_update_printer(app_name: &str) -> ResultType<()> {
let printer_name = crate::get_printer_name(app_name);
let driver_name = crate::get_driver_name();
let port = crate::get_port_name(app_name);
let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr());
let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr());
let rd_printer_port = PCWSTR::from_raw(port.as_ptr());
let inf_file = get_driver_inf_abs_path()?;
let inf_file: Vec<u16> = inf_file
.to_string_lossy()
.as_ref()
.encode_utf16()
.chain(Some(0).into_iter())
.collect();
let _lock = SETUP_MTX.lock().unwrap();
check_add_local_port(&rd_printer_port)?;
let should_install_driver = match get_installed_driver_version(&rd_printer_driver_name)? {
Some(_version) => {
delete_printer(&rd_printer_name)?;
allow_err!(uninstall_driver(&rd_printer_driver_name));
true
}
None => true,
};
if should_install_driver {
allow_err!(install_driver(&rd_printer_driver_name, inf_file.as_ptr()));
}
add_printer(&rd_printer_name, &rd_printer_driver_name, &rd_printer_port)?;
Ok(())
}
pub fn uninstall_printer(app_name: &str) {
let printer_name = crate::get_printer_name(app_name);
let driver_name = crate::get_driver_name();
let port = crate::get_port_name(app_name);
let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr());
let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr());
let rd_printer_port = PCWSTR::from_raw(port.as_ptr());
let _lock = SETUP_MTX.lock().unwrap();
allow_err!(delete_printer(&rd_printer_name));
allow_err!(uninstall_driver(&rd_printer_driver_name));
allow_err!(check_delete_local_port(&rd_printer_port));
}