From 7b96495ddf1ed80f144dab12bb603520a16b6cf6 Mon Sep 17 00:00:00 2001 From: Dmitry Skorodumov Date: Thu, 10 Jul 2025 13:38:31 +0300 Subject: [PATCH] Started implementation of IPP Printer Proxy At the moment it is just a skeleton, which does near nothing. It enumerates CUPS printers once per 30 seconds. All data from UnixPipe chanel is silently consumed and not processed Printer structure is just a stub Signed-off-by: Dmitry Skorodumov Signed-off-by: dskr99 --- ipp_proxy/Cargo.toml | 29 ++ ipp_proxy/src/lib.rs | 93 ++++++ ipp_proxy/src/main.rs | 103 ++++++ ipp_proxy/src/proxy_client/ipp_printer.rs | 51 +++ ipp_proxy/src/proxy_client/ipp_processing.rs | 102 ++++++ ipp_proxy/src/proxy_client/mod.rs | 292 ++++++++++++++++++ .../src/proxy_client/print_api/cups/mod.rs | 93 ++++++ ipp_proxy/src/proxy_client/print_api/mod.rs | 57 ++++ ipp_proxy/src/proxy_client/state_channel.rs | 164 ++++++++++ 9 files changed, 984 insertions(+) create mode 100644 ipp_proxy/Cargo.toml create mode 100644 ipp_proxy/src/lib.rs create mode 100644 ipp_proxy/src/main.rs create mode 100644 ipp_proxy/src/proxy_client/ipp_printer.rs create mode 100644 ipp_proxy/src/proxy_client/ipp_processing.rs create mode 100644 ipp_proxy/src/proxy_client/mod.rs create mode 100644 ipp_proxy/src/proxy_client/print_api/cups/mod.rs create mode 100644 ipp_proxy/src/proxy_client/print_api/mod.rs create mode 100644 ipp_proxy/src/proxy_client/state_channel.rs diff --git a/ipp_proxy/Cargo.toml b/ipp_proxy/Cargo.toml new file mode 100644 index 000000000..f723c1634 --- /dev/null +++ b/ipp_proxy/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ipp_proxy" +version = "0.1.0" +authors = ["Huawei StratoVirt Team"] +edition = "2021" +description = "Provides printer sharing for stratovirt" +license = "Mulan PSL v2" + +[dependencies] +anyhow = "1.0" +byteorder = "1.4.3" +bytes = "1" +clap = { version = "4", default-features = false, features = ["std", "derive"] } +httparse = "1.9.4" +http = { version = "1.3.1", default-features = false } +ipp = { version = "5.2.0", default-features = false } +libc = "0.2.139" +log = "0.4" +serde_json = "1.0.96" +signal-hook = "0.3.17" +signal-hook-registry = "1.4.2" +util = { path = "../util" } +vmm-sys-util = "0.12.1" +strum = "0.24.1" +strum_macros = "0.24.3" +uuid = { version = "1.16.0", features = ["v5"] } + +[features] +testing = [] diff --git a/ipp_proxy/src/lib.rs b/ipp_proxy/src/lib.rs new file mode 100644 index 000000000..1e6cd6d3a --- /dev/null +++ b/ipp_proxy/src/lib.rs @@ -0,0 +1,93 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +pub mod proxy_client; + +use std::{os::unix::net::UnixStream, sync::Arc, thread}; + +use anyhow::Result; +use log::{error, info}; +use proxy_client::ProxyClient; +use vmm_sys_util::eventfd::EventFd; + +pub struct IppProxyManager { + pub state_stream: UnixStream, + pub data_stream: UnixStream, + pub spool_dir: String, + pub exit_evt: Arc, +} + +impl IppProxyManager { + pub fn new( + state_stream: UnixStream, + data_stream: UnixStream, + spool_dir: String, + exit_evt: Arc, + ) -> Self { + Self { + state_stream, + data_stream, + spool_dir, + exit_evt, + } + } + + pub fn realize(&mut self) -> Result<()> { + let ipp_proxy_manager = self.try_clone()?; + if let Err(e) = thread::Builder::new() + .name("ipp-proxy-manager".to_string()) + .spawn(move || { + ipp_proxy_manager_run(ipp_proxy_manager); + }) + { + error!( + "failed to start ipp-proxy-manager thread with error {:?}", + e + ); + } + Ok(()) + } + + fn try_clone(&self) -> Result { + Ok(Self { + state_stream: self.state_stream.try_clone()?, + data_stream: self.data_stream.try_clone()?, + spool_dir: self.spool_dir.clone(), + exit_evt: self.exit_evt.clone(), + }) + } +} + +fn ipp_proxy_manager_run(ipp_proxy_manager: IppProxyManager) { + let IppProxyManager { + state_stream, + data_stream, + spool_dir, + exit_evt, + } = ipp_proxy_manager; + let mut proxy_client = match ProxyClient::new(state_stream, data_stream, &spool_dir, exit_evt) { + Ok(proxy_client) => proxy_client, + Err(e) => { + error!("ipp_proxy_manager: error initializing proxy: {:?}", e); + return; + } + }; + + match proxy_client.run() { + Ok(ret) => { + info!("ipp-proxy-manager: ProxyClient stopped with ret {:?}", ret); + } + Err(ref e) => { + error!("ipp-proxy-manager: Error at ProxyClient::run(): {e}"); + } + } +} diff --git a/ipp_proxy/src/main.rs b/ipp_proxy/src/main.rs new file mode 100644 index 000000000..4aa0058d4 --- /dev/null +++ b/ipp_proxy/src/main.rs @@ -0,0 +1,103 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +mod proxy_client; + +use std::{os::unix::net::UnixStream, process, sync::Arc}; + +use clap::Parser; +use libc::{EFD_NONBLOCK, EFD_SEMAPHORE}; +use log::*; +use proxy_client::*; +use util::logger; +use vmm_sys_util::eventfd::EventFd; + +#[derive(Clone, Debug, Parser)] +#[command( + name = "ipp-proxy", + about = "IPP Proxy for virtual machines shared printers", + args_override_self = true +)] +struct Opt { + #[arg(long, short = 'D', default_value = "")] + log_file: String, + #[arg(long = "log-level", default_value = "info")] + log_level: Level, + #[arg(long = "state-socket", required = true)] + state_socket_path: String, + #[arg(long = "data-socket", required = true)] + data_socket_path: String, + #[arg(long = "spool-dir", required = true)] + spool_dir: String, +} + +fn initialize_logging(opt: &Opt) { + let log_env_string = match opt.log_level { + Level::Error => "error", + Level::Warn => "warn", + Level::Info => "info", + Level::Debug => "debug", + Level::Trace => "trace", + }; + std::env::set_var("STRATOVIRT_LOG_LEVEL", log_env_string); + if let Err(e) = logger::init_log(opt.log_file.clone()) { + println!("can't enable logger: {}", e); + } +} + +fn main() { + let opt = Opt::parse(); + + initialize_logging(&opt); + + let exit_evt = Arc::new( + EventFd::new(EFD_NONBLOCK | EFD_SEMAPHORE).unwrap_or_else(|_| { + error!("killevent create failed"); + process::exit(1); + }), + ); + + // SAFETY: sets signal-handler via signal_hook crate. Just signal the exit_evt in handler. + let _ = unsafe { + let exit_evt = exit_evt.clone(); + signal_hook_registry::register(signal_hook::consts::SIGTERM, move || { + exit_evt.write(0xffffffff).unwrap() + }) + } + .unwrap_or_else(|error| { + error!("Error setup signals: {}", error); + process::exit(1); + }); + + let proxy_state_stream = UnixStream::connect(&opt.state_socket_path).unwrap(); + let proxy_data_stream = UnixStream::connect(&opt.data_socket_path).unwrap(); + let mut proxy_client = ProxyClient::new( + proxy_state_stream, + proxy_data_stream, + &opt.spool_dir, + exit_evt, + ) + .unwrap_or_else(|error| { + error!("Error initializing proxy: {}", error); + process::exit(1); + }); + + match proxy_client.run() { + Ok(ret) => { + info!("ProxyClient stopped with ret {:?}", ret); + } + Err(ref e) => { + error!("Error at ProxyClient::run(): {e}"); + process::exit(1); + } + } +} diff --git a/ipp_proxy/src/proxy_client/ipp_printer.rs b/ipp_proxy/src/proxy_client/ipp_printer.rs new file mode 100644 index 000000000..a86711f1c --- /dev/null +++ b/ipp_proxy/src/proxy_client/ipp_printer.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use ipp::value::IppValue; +use std::collections::HashMap; + +use crate::proxy_client::print_api::{PrintApi, PrintOps, PrinterId}; + +pub struct Printer { + _printer_id: String, + sys_id: PrinterId, + _printer_attributes: PrinterAttrs, + _pagesize_ids: PageSizesIds, +} + +pub type PageSizesIds = Vec<(String, String)>; // Vector of mappings of OhosPageId -> IppPageId +pub type PrinterAttrs = HashMap<&'static str, IppValue>; + +pub fn ipp_create_default_printer_attrs(_sys_id: &PrinterId) -> PrinterAttrs { + PrinterAttrs::new() +} + +impl Printer { + pub fn new(sys_id: &PrinterId) -> Self { + let mut pagesize_ids = PageSizesIds::new(); + let pa = match PrintApi::fill_printer_attributes(sys_id, &mut pagesize_ids) { + Ok(a) => a, + _ => ipp_create_default_printer_attrs(sys_id), + }; + + Self { + _printer_id: sys_id.printer_id.clone(), + sys_id: sys_id.clone(), + _printer_attributes: pa, + _pagesize_ids: pagesize_ids, + } + } + + pub fn get_sys_id(&self) -> PrinterId { + self.sys_id.clone() + } +} diff --git a/ipp_proxy/src/proxy_client/ipp_processing.rs b/ipp_proxy/src/proxy_client/ipp_processing.rs new file mode 100644 index 000000000..bb6ab5035 --- /dev/null +++ b/ipp_proxy/src/proxy_client/ipp_processing.rs @@ -0,0 +1,102 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use std::collections::{HashMap, HashSet}; + +use log::warn; +use uuid::Uuid; + +use crate::proxy_client::{ + ipp_printer::Printer, + print_api::{PrintApi, PrintOps, PrinterId}, + VMGT_UUID_PREFIX, +}; + +pub fn ipp_make_uuid(printer_id: &str) -> String { + let uuid_in = Uuid::new_v5(&Uuid::NAMESPACE_URL, printer_id.as_bytes()); + let mut uuid_bin = uuid_in.into_bytes(); + let vmgt_magic = VMGT_UUID_PREFIX.as_bytes(); + uuid_bin[0..8].copy_from_slice(vmgt_magic); + Uuid::from_bytes(uuid_bin).to_string() +} + +pub struct IppProcessing { + printer_uuid_num: HashMap, + printers_list: HashMap, +} + +impl Default for IppProcessing { + fn default() -> Self { + Self { + printer_uuid_num: HashMap::new(), + printers_list: HashMap::new(), + } + } +} + +impl IppProcessing { + pub fn new() -> Self { + Self::default() + } + + pub fn refresh_printers(&mut self) -> bool { + let mut added_printers: HashSet = HashSet::new(); + let mut removed_printers: HashSet = HashSet::new(); + + let sysid_list = match PrintApi::create_printers_list(&mut self.printer_uuid_num) { + Ok(list) => list, + Err(e) => { + warn!("Failed to create printers list: {:?}", e); + Vec::new() + } + }; + // Create hash-map of printers, based on id + let mut id_map: HashMap = HashMap::new(); + for printer in &sysid_list { + id_map.insert(printer.printer_num, printer.clone()); + } + + // List of new printers + for sysid in &sysid_list { + if self.printers_list.get(&sysid.printer_num).is_none() { + added_printers.insert(sysid.printer_num); + } + } + // List of removed printers + for (printer_id, _) in self.printers_list.iter() { + if id_map.get(printer_id).is_none() { + removed_printers.insert(printer_id.clone()); + } + } + + let changed = !added_printers.is_empty() || !removed_printers.is_empty(); + + for printer_id in added_printers { + let sysid = id_map.get(&printer_id).unwrap(); // guaranteed to be valid + self.printers_list.insert(printer_id, Printer::new(sysid)); + } + + for printer_id in removed_printers { + self.printers_list.remove(&printer_id); + } + + changed + } + + pub fn get_printers_list(&self) -> Vec { + let mut list: Vec = Vec::new(); + for (_, ipp_printer) in self.printers_list.iter() { + list.push(ipp_printer.get_sys_id()); + } + list + } +} diff --git a/ipp_proxy/src/proxy_client/mod.rs b/ipp_proxy/src/proxy_client/mod.rs new file mode 100644 index 000000000..65f598ec4 --- /dev/null +++ b/ipp_proxy/src/proxy_client/mod.rs @@ -0,0 +1,292 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +pub mod ipp_printer; +pub mod ipp_processing; +pub mod print_api; +pub mod state_channel; + +use std::{ + io, + io::{prelude::*, ErrorKind}, + os::fd::AsRawFd, + os::unix::net::UnixStream, + sync::{Arc, RwLock}, + thread, +}; + +use anyhow::{bail, Result}; +use byteorder::{ByteOrder, LittleEndian}; +use log::{error, info, trace}; +use strum::EnumCount; +use strum_macros::EnumCount; +use vmm_sys_util::{ + epoll::{ControlOperation, Epoll, EpollEvent, EventSet}, + eventfd::EventFd, +}; + +use crate::proxy_client::{ + ipp_processing::IppProcessing, + print_api::{PrintApi, PrintOps}, + state_channel::*, +}; + +pub type StateThreadHandle = Option>; + +pub const VMGT_UUID_PREFIX: &str = "VMGT31dc"; + +#[derive(Debug, EnumCount)] +enum ProxyClientEvent { + Stop = 0, + Data, +} + +impl TryFrom for ProxyClientEvent { + type Error = anyhow::Error; + + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(Self::Stop), + 1 => Ok(Self::Data), + other => bail!("Unknown proxy client event: {}", other), + } + } +} + +const VPRINT_DATA_HDR_SIZE: usize = 8; +const VPRINT_MAX_DATA_SIZE: usize = 1024 * 1024; // no more then 1M in one chunk + +struct StreamData { + id: u32, + size: usize, + data_ready: usize, + data: Vec, +} + +pub struct ProxyClient { + _ipp_process: Arc>, + exit_evt: Arc, + need_reset: bool, + data_stream: UnixStream, + state_thread: StateThreadHandle, + _spool_dir: String, // directory for temp files + epoll: Epoll, + // hdr: + // stream_id: u32, + // data_bytes: u32, + hdr_bytes: [u8; VPRINT_DATA_HDR_SIZE], + hdr_bytes_ready: usize, + curr_stream_id: u32, + stream: Option, +} + +impl ProxyClient { + pub fn new( + state_stream: UnixStream, + data_stream: UnixStream, + spool_dir: &String, + exit_evt: Arc, + ) -> Result { + if let Err(e) = PrintApi::init_printers() { + bail!("Failed to initialize printers: {:?}", e); + } + + state_stream.set_nonblocking(true)?; + data_stream.set_nonblocking(true)?; + + let mut ipp_process = IppProcessing::new(); + ipp_process.refresh_printers(); + if ipp_process.get_printers_list().is_empty() { + info!("No printers are installed in system; Will listen for changes.") + } + let ipp_process = Arc::new(RwLock::new(ipp_process)); + let state_thread = start_state_thread(state_stream, ipp_process.clone(), exit_evt.clone())?; + + info!("ProxyClient is initialized"); + + Ok(Self { + _ipp_process: ipp_process, + exit_evt, + need_reset: false, + data_stream, + state_thread, + _spool_dir: spool_dir.to_string(), + epoll: Epoll::new()?, + hdr_bytes: [0; VPRINT_DATA_HDR_SIZE], + hdr_bytes_ready: 0, + curr_stream_id: 0, + stream: None, + }) + } + + pub fn read_buf_helper(stream: &mut UnixStream, buf: &mut [u8]) -> io::Result { + match stream.read(buf) { + Ok(n) => { + if n == 0 { + Err(std::io::Error::new( + std::io::ErrorKind::ConnectionAborted, + "connection closed", + )) + } else { + Ok(n) + } + } + Err(e) => match e.kind() { + ErrorKind::Interrupted | ErrorKind::TimedOut => Ok(0), + ErrorKind::WouldBlock => { + Ok(0) // message not ready yet + } + _ => { + error!("Error at read(): {e}"); + Err(e) + } + }, + } + } + + // Disable all transmits until guest explicitly doesn't reset error. + fn set_data_error(&mut self) -> Result<()> { + let _ = self.epoll.ctl( + ControlOperation::Delete, + self.data_stream.as_raw_fd(), + EpollEvent::default(), + )?; + self.need_reset = true; + Ok(()) + } + + fn check_stream_hdr_ready(&mut self) -> Result { + if self.stream.is_some() { + return Ok(true); + } + self.hdr_bytes_ready += match self + .data_stream + .read(&mut self.hdr_bytes[self.hdr_bytes_ready..VPRINT_DATA_HDR_SIZE]) + { + Ok(n) => { + if n == 0 { + bail!("Connection closed"); + } else { + n + } + } + Err(e) => match e.kind() { + ErrorKind::Interrupted | ErrorKind::WouldBlock => return Ok(false), + _ => { + return Err(e.into()); + } + }, + }; + if self.hdr_bytes_ready != VPRINT_DATA_HDR_SIZE { + return Ok(false); + }; + + let id = LittleEndian::read_u32(&self.hdr_bytes[0..]); + let size = LittleEndian::read_u32(&self.hdr_bytes[4..]) as usize; + if size > VPRINT_MAX_DATA_SIZE { + self.set_data_error()?; + return Ok(false); + }; + let mut data = Vec::with_capacity(size); + // We don't want zero this memory. It will be overwritten anyway. + unsafe { + data.set_len(size); + } + self.stream = Some(StreamData { + id, + size, + data_ready: 0, + data, + }); + Ok(true) + } + + fn handle_data_event(&mut self) -> Result<()> { + if !self.check_stream_hdr_ready()? { + return Ok(()); + } + let stream = &mut self.stream.as_mut().unwrap(); + stream.data_ready += match Self::read_buf_helper( + &mut self.data_stream, + &mut stream.data[stream.data_ready..], + ) { + Ok(n) => n, + Err(e) => { + return Err(e.into()); + } + }; + if stream.data_ready != stream.size { + return Ok(()); + } + let _data = std::mem::take(&mut stream.data); + self.curr_stream_id = stream.id; + self.stream = None; + self.hdr_bytes_ready = 0; + + // just discard data at the moment + Ok(()) + } + + pub fn run(&mut self) -> Result<()> { + self.epoll + .ctl( + ControlOperation::Add, + self.exit_evt.as_raw_fd(), + EpollEvent::new(EventSet::IN, ProxyClientEvent::Stop as u64), + ) + .unwrap(); + + self.epoll + .ctl( + ControlOperation::Add, + self.data_stream.as_raw_fd(), + EpollEvent::new(EventSet::IN, ProxyClientEvent::Data as u64), + ) + .unwrap(); + + let mut events = [EpollEvent::new(EventSet::empty(), 0); ProxyClientEvent::COUNT]; + + 'epoll_loop: loop { + let cnt = match self.epoll.wait(-1, &mut events[..]) { + Err(e) => { + error!("epoll_wait failed: {}", e.kind()); + if e.kind() == io::ErrorKind::Interrupted { + continue; + } + bail!("unknown poll error"); + } + Ok(res) => res, + }; + + for event in events.iter().take(cnt) { + if EventSet::from_bits(event.events()).is_none() { + trace!("epoll: ignoring unknown event set: 0x{:x}", event.events()); + continue; + }; + + match ProxyClientEvent::try_from(event.data())? { + ProxyClientEvent::Stop => { + info!("stopping proxy-client thread"); + break 'epoll_loop; + } + ProxyClientEvent::Data => self.handle_data_event()?, + } + } + } + + info!("stop_state_thread..."); + stop_state_thread(&mut self.state_thread); + info!("state_thread() finished."); + + Ok(()) + } +} diff --git a/ipp_proxy/src/proxy_client/print_api/cups/mod.rs b/ipp_proxy/src/proxy_client/print_api/cups/mod.rs new file mode 100644 index 000000000..f77d26a62 --- /dev/null +++ b/ipp_proxy/src/proxy_client/print_api/cups/mod.rs @@ -0,0 +1,93 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use std::{collections::HashMap, process::Command}; + +use anyhow::Result; +use core::time::Duration; +use log::{error, trace}; +use std::thread; +use vmm_sys_util::eventfd::EventFd; + +use crate::proxy_client::{ + ipp_printer::{ipp_create_default_printer_attrs, PageSizesIds, PrinterAttrs}, + ipp_processing::ipp_make_uuid, + print_api::{PrintOps, PrinterId}, +}; + +pub struct CupsPrintApi {} + +// CUPS printers refresh interval +const PRINTER_REFRESH_INTERVAL_SECS: u64 = 30; + +impl PrintOps for CupsPrintApi { + fn init_printers() -> Result<()> { + Ok(()) + } + + fn create_printers_list(printer_uuid_num: &mut HashMap) -> Result> { + trace!("Creating printers list"); + + let command = Command::new("lpstat") + .arg("-a") + .output() + .unwrap_or_else(|e| { + error!("Error at lpstat -a: {e}"); + std::process::exit(1); + }); + + let mut printers_list: Vec = Vec::new(); + let output = String::from_utf8(command.stdout)?; + + for line in output.lines() { + if let Some(name) = line.split_whitespace().next() { + let converted_printer_id = ipp_make_uuid(name); + let printer_num = if printer_uuid_num.contains_key(&converted_printer_id) { + printer_uuid_num + .get(&converted_printer_id) + .copied() + .unwrap() + } else { + let printer_num = printer_uuid_num.len() as u32; + printer_uuid_num.insert(converted_printer_id.clone(), printer_num); + printer_num + }; + + printers_list.push(PrinterId::new( + printer_num, + &converted_printer_id, + name, + &format!("CUPS {name}"), + )); + } + } + + trace!("Printers list created"); + + Ok(printers_list) + } + + fn subscribe_printers_changes(update_evt: EventFd) -> Result<()> { + thread::spawn(move || loop { + thread::sleep(Duration::from_secs(PRINTER_REFRESH_INTERVAL_SECS)); + let _ = update_evt.write(1); + }); + Ok(()) + } + + fn fill_printer_attributes( + sys_id: &PrinterId, + _pagesize_ids: &mut PageSizesIds, + ) -> Result { + Ok(ipp_create_default_printer_attrs(sys_id)) + } +} diff --git a/ipp_proxy/src/proxy_client/print_api/mod.rs b/ipp_proxy/src/proxy_client/print_api/mod.rs new file mode 100644 index 000000000..1d8620746 --- /dev/null +++ b/ipp_proxy/src/proxy_client/print_api/mod.rs @@ -0,0 +1,57 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +mod cups; +pub type PrintApi = cups::CupsPrintApi; + +use std::collections::HashMap; + +use anyhow::Result; +use util::byte_code::ByteCode; +use vmm_sys_util::eventfd::EventFd; + +use crate::proxy_client::ipp_printer::{PageSizesIds, PrinterAttrs}; + +pub trait PrintOps { + fn init_printers() -> Result<()>; + fn create_printers_list(printer_uuid_num: &mut HashMap) -> Result>; + fn subscribe_printers_changes(update_evt: EventFd) -> Result<()>; + fn fill_printer_attributes( + sys_id: &PrinterId, + pagesize_ids: &mut PageSizesIds, + ) -> Result; +} + +#[derive(Clone, Default, Debug)] +pub struct PrinterId { + /// A printer NUM used to identify printer in printer manager + pub printer_num: u32, + /// A printer ID for ipp-url. + pub printer_id: String, + /// A printer ID used to identify printer on host. + pub _host_printer_id: String, + /// A name of the printer. + pub _printer_name: String, +} + +impl ByteCode for PrinterId {} + +impl PrinterId { + pub fn new(printer_num: u32, printer_id: &str, host_printer_id: &str, name: &str) -> Self { + Self { + printer_num, + printer_id: printer_id.to_string(), + _host_printer_id: host_printer_id.to_string(), + _printer_name: name.to_string(), + } + } +} diff --git a/ipp_proxy/src/proxy_client/state_channel.rs b/ipp_proxy/src/proxy_client/state_channel.rs new file mode 100644 index 000000000..42cb4687a --- /dev/null +++ b/ipp_proxy/src/proxy_client/state_channel.rs @@ -0,0 +1,164 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use std::{ + io, + os::fd::AsRawFd, + os::unix::net::UnixStream, + sync::{Arc, RwLock}, +}; + +use anyhow::{bail, Context, Result}; +use libc::EFD_NONBLOCK; +use log::{error, info, trace}; +use strum::EnumCount; +use strum_macros::EnumCount; +use vmm_sys_util::{ + epoll::{ControlOperation, Epoll, EpollEvent, EventSet}, + eventfd::EventFd, +}; + +use crate::proxy_client::{ + ipp_processing::IppProcessing, + print_api::{PrintApi, PrintOps}, + StateThreadHandle, +}; + +#[derive(Debug, EnumCount)] +enum StateChannelEvent { + Update = 0, + Stop, +} + +impl TryFrom for StateChannelEvent { + type Error = anyhow::Error; + + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(Self::Update), + 1 => Ok(Self::Stop), + other => bail!("Unknown state channel event: {}", other), + } + } +} + +pub struct StateChannel { + ipp_process: Arc>, + update_evt: Arc, + exit_evt: Arc, + epoll: Epoll, +} + +impl StateChannel { + pub fn new( + ipp_process: Arc>, + _state_stream: UnixStream, + exit_evt: Arc, + ) -> Result { + let update_evt = Arc::new(EventFd::new(EFD_NONBLOCK)?); + PrintApi::subscribe_printers_changes(update_evt.try_clone().unwrap())?; + + Ok(Self { + ipp_process, + update_evt, + exit_evt, + epoll: Epoll::new()?, + }) + } + + fn handle_update_event(&mut self) -> Result<()> { + info!("printers update notification"); + // Clear the event. + let _ = self.update_evt.read(); + let ipp_process = self.ipp_process.clone(); + std::thread::Builder::new() + .name("ipp proxy printer refresh".to_string()) + .spawn(move || refresh_printers_job(ipp_process)) + .with_context(|| "Failed to create refresh printer thread")?; + Ok(()) + } + + pub fn run(&mut self) -> Result<()> { + self.epoll.ctl( + ControlOperation::Add, + self.exit_evt.try_clone().unwrap().as_raw_fd(), + EpollEvent::new(EventSet::IN, StateChannelEvent::Stop as u64), + )?; + + self.epoll.ctl( + ControlOperation::Add, + self.update_evt.as_raw_fd(), + EpollEvent::new(EventSet::IN, StateChannelEvent::Update as u64), + )?; + + let mut events = [EpollEvent::new(EventSet::empty(), 0); StateChannelEvent::COUNT]; + 'epoll_loop: loop { + let cnt = match self.epoll.wait(-1, &mut events[..]) { + Err(e) => { + error!("epoll_wait failed: {}", e.kind()); + if e.kind() == io::ErrorKind::Interrupted { + continue; + } + bail!("unknown poll error"); + } + Ok(res) => res, + }; + + for event in events.iter().take(cnt) { + if EventSet::from_bits(event.events()).is_none() { + trace!("epoll: ignoring unknown event set: 0x{:x}", event.events()); + continue; + } + + match StateChannelEvent::try_from(event.data())? { + StateChannelEvent::Update => self.handle_update_event()?, + StateChannelEvent::Stop => { + info!("stopping proxy-client thread"); + break 'epoll_loop; + } + } + } + } + + info!("PrinterListSender is stopped"); + Ok(()) + } +} + +pub fn start_state_thread( + state_stream: UnixStream, + ipp_process: Arc>, + exit_evt: Arc, +) -> Result { + let join_handle = std::thread::Builder::new() + .name("printers-state".to_string()) + .spawn(move || { + if let Err(e) = StateChannel::new(ipp_process, state_stream, exit_evt) + .unwrap() + .run() + { + error!("State channel state exited with error: {:?}", e); + } + }) + .with_context(|| "Failed to create printers-state thread")?; + Ok(Some(join_handle)) +} + +pub fn stop_state_thread(h: &mut StateThreadHandle) { + if h.is_some() && h.take().unwrap().join().is_err() { + error!("Error at stop of printer-state thread."); + } +} + +fn refresh_printers_job(ipp_process: Arc>) { + ipp_process.write().unwrap().refresh_printers(); +} -- Gitee