Enhance printing functionality with SNMP support and update dependencies

- Added SNMP configuration options to `AppConfig` for querying printer status.
- Extended `PrintInstruction` in the proto file to support additional fields for remote file URLs and raw data.
- Implemented logic in `CupsClient` to retrieve printer paper levels and page counts via SNMP.
- Updated `Cargo.toml` to include `snmp` and `reqwest` dependencies for network communication.
- Refactored `TempPdf` handling to support multiple data sources for printing.
- Improved error handling and logging for better diagnostics during print operations.
This commit is contained in:
lan
2025-12-14 22:04:27 +08:00
parent 5789a4346a
commit 3a9aaef996
12 changed files with 1515 additions and 86 deletions

271
src/bin/cups_smoke.rs Normal file
View File

@@ -0,0 +1,271 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use print_backend::{
config::{AppConfig, LoadBalanceStrategy},
models::{ColorModeSetting, DuplexSetting, OrientationSetting, PrintParams, QualitySetting},
printer::{CupsClient, SharedPrinterAdapter},
scheduler::Scheduler,
};
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse(std::env::args().skip(1))?;
let config_path = std::env::var("CONFIG_PATH").ok();
let config = AppConfig::load(config_path.as_deref())?;
configure_cups(&config)?;
let adapter: SharedPrinterAdapter = Arc::new(CupsClient::with_snmp(Some(config.snmp.clone())));
let scheduler = Arc::new(Scheduler::new(
config.scheduler.strategy,
config.scheduler.refresh_interval_secs,
adapter,
config.scheduler.default_ppm_bw,
config.scheduler.default_ppm_color,
config.scheduler.target_wait_minutes,
config.scheduler.printer_speeds.clone(),
));
if args.list {
list_printers(&scheduler).await?;
return Ok(());
}
let file = args.file.clone();
if let Some(file) = file {
let params = args.into_print_params();
let retry = &config.retry;
let defaults = &config.defaults;
let target = params.printer.as_deref();
let (job, retries_used) = scheduler
.dispatch_with_target(file.as_path(), &params, defaults, retry, target)
.await
.context("提交打印任务失败")?;
println!(
"打印成功: printer={}, job_id={}, retries_used={}",
job.printer, job.job_id, retries_used
);
return Ok(());
}
Args::print_usage();
Err(anyhow!("未指定操作 (--list 或 --print)"))
}
fn configure_cups(config: &AppConfig) -> Result<()> {
if let Some(server) = &config.cups.server {
let normalized = normalize_server(server);
unsafe {
std::env::set_var("CUPS_SERVER", &normalized);
}
cups_rs::config::set_server(Some(&normalized)).context("设置 CUPS 服务器失败")?;
}
if let Some(user) = &config.cups.user {
unsafe {
std::env::set_var("CUPS_USER", user);
}
cups_rs::config::set_user(Some(user)).context("设置 CUPS 用户失败")?;
}
if let Some(password) = &config.cups.password {
unsafe {
std::env::set_var("CUPS_PASSWORD", password);
}
}
Ok(())
}
fn normalize_server(input: &str) -> String {
if let Some((_, rest)) = input.split_once("://") {
rest.split('/').next().unwrap_or(rest).to_string()
} else {
input.split('/').next().unwrap_or(input).to_string()
}
}
async fn list_printers(scheduler: &Scheduler) -> Result<()> {
let printers = scheduler
.refresh_snapshot()
.await
.context("获取打印机列表失败")?;
if printers.is_empty() {
println!("未发现打印机");
return Ok(());
}
println!("发现 {} 台打印机:", printers.len());
for p in printers {
println!(
"- name: {}\n id: {}\n state: {:?}\n accepting_jobs: {}\n active_jobs: {}\n color_supported: {}\n ppm: {:?}, ppm_color: {:?}\n paper_percent: {:?}\n page_count: {:?}\n reasons: {:?}\n",
p.dest.name,
p.id,
p.state,
p.accepting_jobs,
p.active_jobs,
p.color_supported,
p.ppm,
p.ppm_color,
p.paper_percent,
p.page_count,
p.reasons
);
}
Ok(())
}
#[derive(Debug, Default)]
struct Args {
list: bool,
file: Option<PathBuf>,
printer: Option<String>,
copies: Option<u32>,
color: Option<ColorModeSetting>,
duplex: Option<DuplexSetting>,
media: Option<String>,
quality: Option<QualitySetting>,
orientation: Option<OrientationSetting>,
job_name: Option<String>,
}
impl Args {
fn parse<I: Iterator<Item = String>>(mut iter: I) -> Result<Self> {
let mut args = Args::default();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--list" => args.list = true,
"--print" | "-f" | "--file" => {
let v = iter.next().ok_or_else(|| anyhow!("--print 需要文件路径"))?;
args.file = Some(PathBuf::from(v));
}
"--printer" | "-p" => {
args.printer =
Some(iter.next().ok_or_else(|| anyhow!("--printer 需要值"))?);
}
"--copies" => {
let v = iter.next().ok_or_else(|| anyhow!("--copies 需要值"))?;
args.copies = Some(v.parse()?);
}
"--color" => {
let v = iter.next().ok_or_else(|| anyhow!("--color 需要值"))?;
args.color = Some(parse_color(&v)?);
}
"--duplex" => {
let v = iter.next().ok_or_else(|| anyhow!("--duplex 需要值"))?;
args.duplex = Some(parse_duplex(&v)?);
}
"--media" => {
args.media = Some(iter.next().ok_or_else(|| anyhow!("--media 需要值"))?);
}
"--quality" => {
let v = iter.next().ok_or_else(|| anyhow!("--quality 需要值"))?;
args.quality = Some(parse_quality(&v)?);
}
"--orientation" => {
let v = iter.next().ok_or_else(|| anyhow!("--orientation 需要值"))?;
args.orientation = Some(parse_orientation(&v)?);
}
"--job-name" => {
args.job_name =
Some(iter.next().ok_or_else(|| anyhow!("--job-name 需要值"))?);
}
"--strategy" => {
// keep compatibility with config; no-op but accept for clarity
let v = iter.next().ok_or_else(|| anyhow!("--strategy 需要值"))?;
if v.to_lowercase() == "round_robin" {
args.set_strategy(LoadBalanceStrategy::RoundRobin)?;
} else if v.to_lowercase() == "least_queued" {
args.set_strategy(LoadBalanceStrategy::LeastQueued)?;
} else {
return Err(anyhow!(
"不支持的策略: {} (可选: round_robin, least_queued)",
v
));
}
}
"--help" | "-h" => {
Args::print_usage();
std::process::exit(0);
}
other => {
return Err(anyhow!("未知参数: {}", other));
}
}
}
Ok(args)
}
fn into_print_params(mut self) -> PrintParams {
let mut params = PrintParams {
copies: self.copies,
duplex: self.duplex,
color: self.color,
media: self.media.take(),
quality: self.quality,
orientation: self.orientation,
job_name: self.job_name.take(),
printer: self.printer.take(),
};
// convenience: allow -p to set printer
if params.printer.is_none() {
params.printer = self.printer.take();
}
params
}
fn set_strategy(&mut self, strategy: LoadBalanceStrategy) -> Result<()> {
// Currently strategy is only configurable via config; accept CLI flag but ensure config matches.
if strategy != LoadBalanceStrategy::LeastQueued {
// Most users rely on config; warn if they expect change.
println!("提示:调度策略请在 config.app.yaml 的 scheduler.strategy 中配置");
}
Ok(())
}
fn print_usage() {
println!(
"用法:\n cups_smoke --list\n cups_smoke --print <文件路径> [选项]\n\n\
选项:\n --printer, -p <名称> 指定打印机名称\n --copies <n> 份数\n --color <auto|color|monochrome>\n --duplex <one_sided|two_sided_long_edge|two_sided_short_edge>\n --media <介质>\n --quality <draft|normal|high>\n --orientation <portrait|landscape>\n --job-name <名称>\n --help, -h 显示帮助"
);
}
}
fn parse_color(input: &str) -> Result<ColorModeSetting> {
match input.to_ascii_lowercase().as_str() {
"auto" => Ok(ColorModeSetting::Auto),
"color" => Ok(ColorModeSetting::Color),
"monochrome" | "mono" | "bw" => Ok(ColorModeSetting::Monochrome),
other => Err(anyhow!("无效的 color: {}", other)),
}
}
fn parse_duplex(input: &str) -> Result<DuplexSetting> {
match input.to_ascii_lowercase().as_str() {
"one_sided" | "single" => Ok(DuplexSetting::OneSided),
"two_sided_long_edge" | "long" => Ok(DuplexSetting::TwoSidedLongEdge),
"two_sided_short_edge" | "short" => Ok(DuplexSetting::TwoSidedShortEdge),
other => Err(anyhow!("无效的 duplex: {}", other)),
}
}
fn parse_quality(input: &str) -> Result<QualitySetting> {
match input.to_ascii_lowercase().as_str() {
"draft" => Ok(QualitySetting::Draft),
"normal" => Ok(QualitySetting::Normal),
"high" => Ok(QualitySetting::High),
other => Err(anyhow!("无效的 quality: {}", other)),
}
}
fn parse_orientation(input: &str) -> Result<OrientationSetting> {
match input.to_ascii_lowercase().as_str() {
"portrait" => Ok(OrientationSetting::Portrait),
"landscape" => Ok(OrientationSetting::Landscape),
other => Err(anyhow!("无效的 orientation: {}", other)),
}
}

View File

@@ -13,6 +13,8 @@ pub struct AppConfig {
#[serde(default)]
pub cups: CupsConfig,
#[serde(default)]
pub snmp: SnmpConfig,
#[serde(default)]
pub scheduler: SchedulerConfig,
#[serde(default)]
pub retry: RetryConfig,
@@ -38,6 +40,22 @@ pub struct CupsConfig {
pub password: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SnmpConfig {
/// 是否启用 SNMP 查询耗材/纸量
#[serde(default = "default_snmp_enabled")]
pub enabled: bool,
/// SNMP community默认 public
#[serde(default = "default_snmp_community")]
pub community: String,
/// 查询超时(毫秒)
#[serde(default = "default_snmp_timeout_ms")]
pub timeout_ms: u64,
/// 重试次数
#[serde(default = "default_snmp_retries")]
pub retries: u32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SchedulerConfig {
#[serde(default = "default_strategy")]
@@ -59,7 +77,7 @@ pub struct SchedulerConfig {
pub printer_speeds: std::collections::HashMap<String, PrinterSpeedOverride>,
}
#[derive(Debug, Clone, Deserialize, Copy)]
#[derive(Debug, Clone, Deserialize, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LoadBalanceStrategy {
RoundRobin,
@@ -104,6 +122,17 @@ impl Default for CupsConfig {
}
}
impl Default for SnmpConfig {
fn default() -> Self {
Self {
enabled: default_snmp_enabled(),
community: default_snmp_community(),
timeout_ms: default_snmp_timeout_ms(),
retries: default_snmp_retries(),
}
}
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self {
@@ -161,6 +190,22 @@ fn default_refresh_secs() -> u64 {
10
}
fn default_snmp_enabled() -> bool {
false
}
fn default_snmp_community() -> String {
"public".to_string()
}
fn default_snmp_timeout_ms() -> u64 {
1500
}
fn default_snmp_retries() -> u32 {
1
}
fn default_ppm_bw() -> u32 {
30
}

View File

@@ -1,12 +1,15 @@
use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration};
use std::{
collections::HashMap,
io::Write,
path::PathBuf,
sync::Arc,
time::Duration,
};
use anyhow::{anyhow, Context, Result};
use tempfile::NamedTempFile;
use tokio::{
io::AsyncWriteExt,
task::JoinHandle,
time::{sleep, timeout},
};
use tokio::io::AsyncReadExt;
use tokio::time::sleep;
use tokio_stream::wrappers::ReceiverStream;
use tracing::{info, warn};
@@ -22,6 +25,7 @@ use crate::{
scheduler::Scheduler,
};
use cups_rs::PrinterState;
use reqwest::StatusCode;
const RECONNECT_BASE_MS: u64 = 1000;
const RECONNECT_MAX_MS: u64 = 10000;
@@ -73,7 +77,11 @@ impl ControlService {
hostname_value.clone()
};
// send hello
info!(
client_id = %client_id_value,
printers = printer_payload.len(),
"发送 hello 到控制端"
);
let hello = ClientMessage {
msg: Some(client_message::Msg::Hello(Hello {
client_id: client_id_value,
@@ -138,7 +146,7 @@ async fn handle_print(
.ok_or_else(|| anyhow!("缺少打印参数"))?;
let params = map_params(params_proto)?;
let temp = TempPdf::new(instr.pdf_data).await?;
let temp = materialize_pdf(&instr).await?;
let result = scheduler
.dispatch(
temp.path(),
@@ -265,6 +273,8 @@ fn to_proto_printer(p: &PrinterInfo) -> ProtoPrinter {
ppm: p.ppm.unwrap_or_default(),
ppm_color: p.ppm_color.unwrap_or_default(),
reasons: p.reasons.clone(),
paper_percent: p.paper_percent.unwrap_or(-1),
page_count: p.page_count.unwrap_or(0),
}
}
@@ -325,37 +335,17 @@ fn map_params(params: &ProtoPrintParams) -> Result<PrintParams> {
struct TempPdf {
path: PathBuf,
_handle: JoinHandle<Result<()>>,
_file: NamedTempFile,
}
impl TempPdf {
async fn new(data: Vec<u8>) -> Result<Self> {
let (tx, rx) = tokio::sync::oneshot::channel();
let handle = tokio::spawn(async move {
let file = NamedTempFile::new().context("创建临时文件失败")?;
let mut writer = tokio::fs::File::from_std(file.reopen().context("打开临时文件失败")?);
writer
.write_all(&data)
.await
.context("写入临时文件失败")?;
writer.flush().await.context("刷新临时文件失败")?;
let path = file.path().to_path_buf();
// Hold file until drop to keep on disk
tx.send(path).ok();
// keep file alive
sleep(Duration::from_secs(300)).await;
Ok(())
});
let path = timeout(Duration::from_secs(5), rx)
.await
.context("等待临时文件路径超时")?
.context("获取临时文件路径失败")?;
Ok(Self {
path,
_handle: handle,
})
async fn from_bytes(data: &[u8]) -> Result<Self> {
let mut file = NamedTempFile::new().context("创建临时文件失败")?;
file.write_all(data)
.context("写入临时文件失败")?;
file.flush().context("刷新临时文件失败")?;
let path = file.path().to_path_buf();
Ok(Self { path, _file: file })
}
fn path(&self) -> &std::path::Path {
@@ -368,3 +358,48 @@ fn hostname() -> Result<String> {
.map(|s| s.to_string_lossy().to_string())
.context("读取主机名失败")
}
async fn materialize_pdf(instr: &PrintInstruction) -> Result<TempPdf> {
if !instr.pdf_data.is_empty() {
return TempPdf::from_bytes(&instr.pdf_data).await;
}
if !instr.raw_data.is_empty() {
return TempPdf::from_bytes(&instr.raw_data).await;
}
if !instr.url.is_empty() {
return TempPdf::from_url(&instr.url).await;
}
if !instr.local_path.is_empty() {
return TempPdf::from_path(&instr.local_path).await;
}
Err(anyhow!("未提供可用的打印数据pdf_data/raw_data/url/local_path"))
}
impl TempPdf {
async fn from_url(url: &str) -> Result<Self> {
let resp = reqwest::get(url).await.context("下载打印文件失败")?;
if resp.status() == StatusCode::NO_CONTENT {
return Err(anyhow!("下载文件为空"));
}
let resp = resp.error_for_status().context("下载文件返回错误状态")?;
let bytes = resp.bytes().await.context("读取下载内容失败")?;
if bytes.is_empty() {
return Err(anyhow!("下载文件为空"));
}
Self::from_bytes(&bytes).await
}
async fn from_path(path: &str) -> Result<Self> {
let mut file = tokio::fs::File::open(path)
.await
.with_context(|| format!("打开打印文件失败: {}", path))?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)
.await
.with_context(|| format!("读取打印文件失败: {}", path))?;
if buf.is_empty() {
return Err(anyhow!("读取文件为空"));
}
Self::from_bytes(&buf).await
}
}

8
src/lib.rs Normal file
View File

@@ -0,0 +1,8 @@
pub mod config;
pub mod control_client;
pub mod models;
pub mod printer;
pub mod proto;
pub mod scheduler;

View File

@@ -1,16 +1,12 @@
mod config;
mod control_client;
mod models;
mod printer;
mod proto;
mod scheduler;
use std::sync::Arc;
use anyhow::Context;
use control_client::ControlService;
use printer::{CupsClient, SharedPrinterAdapter};
use scheduler::Scheduler;
use print_backend::{
config,
control_client::ControlService,
printer::{CupsClient, SharedPrinterAdapter},
scheduler::Scheduler,
};
use tracing_subscriber::{fmt, EnvFilter};
#[tokio::main]
@@ -41,7 +37,7 @@ async fn main() -> Result<(), anyhow::Error> {
}
}
let adapter: SharedPrinterAdapter = Arc::new(CupsClient::default());
let adapter: SharedPrinterAdapter = Arc::new(CupsClient::with_snmp(Some(config.snmp.clone())));
let scheduler = Arc::new(Scheduler::new(
config.scheduler.strategy,
config.scheduler.refresh_interval_secs,

View File

@@ -1,4 +1,4 @@
use std::{path::Path, sync::Arc};
use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
use anyhow::{Context, Result};
use async_trait::async_trait;
@@ -12,6 +12,7 @@ use tokio::task::spawn_blocking;
use tracing::warn;
use url::Url;
use crate::config::SnmpConfig;
use crate::models::{PrintDefaults, PrintParams};
#[derive(Debug, Clone)]
@@ -27,6 +28,8 @@ pub struct PrinterInfo {
pub device_uri: Option<String>,
pub host: Option<String>,
pub accepting_jobs: bool,
pub paper_percent: Option<i32>,
pub page_count: Option<u64>,
}
impl PrinterInfo {
@@ -65,12 +68,21 @@ pub trait PrintAdapter: Send + Sync {
}
#[derive(Debug, Default)]
pub struct CupsClient;
pub struct CupsClient {
snmp: Option<SnmpConfig>,
}
impl CupsClient {
pub fn with_snmp(snmp: Option<SnmpConfig>) -> Self {
Self { snmp }
}
}
#[async_trait]
impl PrintAdapter for CupsClient {
async fn list_printers(&self) -> Result<Vec<PrinterInfo>> {
spawn_blocking(|| {
let snmp_cfg = self.snmp.clone();
spawn_blocking(move || {
let destinations = get_all_destinations().context("获取打印机列表失败")?;
let mut infos = Vec::with_capacity(destinations.len());
@@ -78,14 +90,7 @@ impl PrintAdapter for CupsClient {
let state = dest.state();
let reasons = dest.state_reasons();
let options = dest.get_options();
let color_supported = options
.get("print-color-mode-supported")
.map(|modes| {
modes
.split(',')
.any(|m| m.trim().eq_ignore_ascii_case("color"))
})
.unwrap_or(true);
let color_supported = detect_color_supported(&options);
let ppm = parse_speed_option(options.get("printer-speed"));
let ppm_color = parse_speed_option(
options
@@ -107,6 +112,17 @@ impl PrintAdapter for CupsClient {
let id = derive_printer_id(&dest, device_uri.as_deref(), host.as_deref());
let accepting_jobs = dest.is_accepting_jobs();
let (paper_percent, page_count) = match (&snmp_cfg, host.as_deref()) {
(Some(snmp_cfg), Some(h)) if snmp_cfg.enabled => match poll_snmp_paper_page(h, snmp_cfg) {
Ok(v) => v,
Err(e) => {
warn!("SNMP 查询 {} 失败: {}", h, e);
(None, None)
}
},
_ => (None, None),
};
infos.push(PrinterInfo {
id,
dest,
@@ -119,6 +135,8 @@ impl PrintAdapter for CupsClient {
device_uri,
host,
accepting_jobs,
paper_percent,
page_count,
});
}
Ok::<_, anyhow::Error>(infos)
@@ -213,8 +231,195 @@ impl PartialEq for PrinterInfo {
&& self.device_uri == other.device_uri
&& self.host == other.host
&& self.accepting_jobs == other.accepting_jobs
&& self.paper_percent == other.paper_percent
&& self.page_count == other.page_count
}
}
impl Eq for PrinterInfo {}
fn detect_color_supported(options: &std::collections::HashMap<String, String>) -> bool {
// Prefer explicit color mode indicators; otherwise be conservative (default false).
for key in [
"print-color-mode-supported",
"printer-color-mode-supported",
"color-supported",
] {
if let Some(v) = options.get(key) {
let has_color = v
.split(|c| c == ',' || c == ' ' || c == ';')
.any(|m| {
let l = m.trim().to_ascii_lowercase();
l == "color" || l.contains("rgb") || l.contains("cmyk")
});
return has_color;
}
}
if let Some(v) = options.get("ColorModel").or_else(|| options.get("color-models")) {
let has_color = v
.split(|c| c == ',' || c == ' ' || c == ';')
.any(|m| {
let l = m.trim().to_ascii_lowercase();
l.contains("rgb") || l.contains("cmyk") || l == "color"
});
return has_color;
}
false
}
fn poll_snmp_paper_page(host: &str, cfg: &SnmpConfig) -> Result<(Option<i32>, Option<u64>)> {
use snmp::SyncSession;
let addr = format!("{host}:161");
let timeout = Duration::from_millis(cfg.timeout_ms);
let mut session = SyncSession::new(
addr.as_str(),
cfg.community.as_bytes(),
Some(timeout),
cfg.retries as i32,
)
.context("创建 SNMP 会话失败")?;
let paper = compute_percent(
&mut session,
&[1, 3, 6, 1, 2, 1, 43, 8, 2, 1, 9, 1], // max: prtInputMaxCapacity
&[1, 3, 6, 1, 2, 1, 43, 8, 2, 1, 10, 1], // level: prtInputCurrentLevel
);
let page_count = read_counter_first(
&mut session,
&[1, 3, 6, 1, 2, 1, 43, 10, 2, 1, 4, 1], // prtMarkerLifeCount
);
Ok((paper, page_count))
}
fn compute_percent(
session: &mut snmp::SyncSession,
max_oid: &[u32],
level_oid: &[u32],
) -> Option<i32> {
use snmp::{ObjIdBuf, Value};
let max_map = walk_i32(session, max_oid);
if max_map.is_empty() {
return None;
}
let mut best: Option<i32> = None;
let mut cursor = level_oid.to_vec();
loop {
let pdu = match session.getbulk(&[cursor.as_slice()], 0, 10) {
Ok(p) => p,
Err(_) => break,
};
let mut progressed = false;
for (oid, val) in pdu.varbinds {
let mut buf: ObjIdBuf = [0; 128];
let name = match oid.read_name(&mut buf) {
Ok(n) => n.to_vec(),
Err(_) => continue,
};
if !name.starts_with(level_oid) {
return best;
}
progressed = true;
cursor = name.clone();
if let Value::Integer(level) = val {
if level <= 0 {
continue;
}
// find matching max by full oid or last index
let max_v = max_map
.get(&name)
.or_else(|| {
name.last().and_then(|last| {
max_map
.iter()
.find(|(k, _)| k.last() == Some(last))
.map(|(_, v)| v)
})
});
if let Some(max_v) = max_v {
if *max_v > 0 {
let pct = ((level as f64) * 100.0 / (*max_v as f64)).round() as i32;
best = Some(best.map_or(pct, |b| b.min(pct)));
}
}
}
}
if !progressed {
break;
}
}
best
}
fn walk_i32(session: &mut snmp::SyncSession, base: &[u32]) -> HashMap<Vec<u32>, i32> {
use snmp::{ObjIdBuf, Value};
let mut results = HashMap::new();
let mut cursor = base.to_vec();
loop {
let pdu = match session.getbulk(&[cursor.as_slice()], 0, 10) {
Ok(p) => p,
Err(_) => break,
};
let mut progressed = false;
for (oid, val) in pdu.varbinds.clone() {
let mut buf: ObjIdBuf = [0; 128];
let name = match oid.read_name(&mut buf) {
Ok(n) => n.to_vec(),
Err(_) => continue,
};
if !name.starts_with(base) {
return results;
}
progressed = true;
cursor = name.clone();
if let Value::Integer(v) = val {
if v > 0 && v <= i32::MAX as i64 {
results.insert(name, v as i32);
}
}
}
if !progressed {
break;
}
}
results
}
fn read_counter_first(session: &mut snmp::SyncSession, base: &[u32]) -> Option<u64> {
use snmp::{ObjIdBuf, Value};
let mut cursor = base.to_vec();
loop {
let pdu = match session.getbulk(&[cursor.as_slice()], 0, 10) {
Ok(p) => p,
Err(_) => break,
};
let mut progressed = false;
for (oid, val) in pdu.varbinds.clone() {
let mut buf: ObjIdBuf = [0; 128];
let name = match oid.read_name(&mut buf) {
Ok(n) => n.to_vec(),
Err(_) => continue,
};
if !name.starts_with(base) {
return None;
}
progressed = true;
cursor = name.clone();
match val {
Value::Integer(v) if v >= 0 => return Some(v as u64),
Value::Counter32(v) => return Some(v as u64),
Value::Counter64(v) => return Some(v),
_ => continue,
}
}
if !progressed {
break;
}
}
None
}

View File

@@ -352,6 +352,8 @@ mod tests {
device_uri: None,
host: None,
accepting_jobs: true,
paper_percent: None,
page_count: None,
}
}