diff --git a/escl-scan-cli/src/main.rs b/escl-scan-cli/src/main.rs index b442c20..0fafbb0 100644 --- a/escl-scan-cli/src/main.rs +++ b/escl-scan-cli/src/main.rs @@ -7,6 +7,7 @@ extern crate scan; use clap::{Args, Parser}; use scan::scanner::Scanner; +use scan::scannerfinder::ScannerFinder; use std::path::Path; use std::process::exit; @@ -39,6 +40,33 @@ struct DeviceArgs { /// Print information about the scanner identified by device name #[arg(short, long)] info: Option, + + /// List available scanners + #[arg(short, long)] + list: bool, +} + +fn list_scanners() { + let mut finder = ScannerFinder::new(); + let scanners = match finder.find(None) { + Ok(scanners) => scanners, + Err(err) => { + eprintln!("Failed to discover scanners: {err}"); + exit(1); + } + }; + + if scanners.len() == 0 { + println!("No scanners found"); + } else if scanners.len() == 1 { + println!("Found 1 scanner:"); + } else { + println!("Found {} scanners:", scanners.len()); + } + + for scanner in scanners { + println!("- {}: {}", scanner.device_name, scanner.base_url); + } } fn get_scanner(cli: &Cli) -> Scanner { @@ -53,6 +81,11 @@ fn main() { env_logger::init(); let args = Cli::parse(); + if args.device.list { + list_scanners(); + exit(0); + } + // TODO This is just a band-aid for testing if let Some(name) = args.device.info { exit(0); diff --git a/escl-scan/Cargo.toml b/escl-scan/Cargo.toml index 5679ddb..63d9db7 100644 --- a/escl-scan/Cargo.toml +++ b/escl-scan/Cargo.toml @@ -20,3 +20,4 @@ log = "0.4.*" reqwest = { version = "0.11.16", features = ["blocking"] } serde = { version = "1.0.160", features = ["derive"] } serde-xml-rs = "0.6.0" +zeroconf = "0.12.*" diff --git a/escl-scan/src/lib.rs b/escl-scan/src/lib.rs index 59f22be..ee61e3c 100644 --- a/escl-scan/src/lib.rs +++ b/escl-scan/src/lib.rs @@ -2,6 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +extern crate zeroconf; + pub mod scanner; pub mod scannererror; +pub mod scannerfinder; pub mod structs; diff --git a/escl-scan/src/scanner.rs b/escl-scan/src/scanner.rs index c93281f..1edfe81 100644 --- a/escl-scan/src/scanner.rs +++ b/escl-scan/src/scanner.rs @@ -13,6 +13,7 @@ use crate::{ use reqwest::blocking::Response; use std::fs::File; +#[derive(Clone, Debug)] pub struct Scanner { pub base_url: String, pub device_name: String, diff --git a/escl-scan/src/scannererror.rs b/escl-scan/src/scannererror.rs index 0ad2927..d8c0ddf 100644 --- a/escl-scan/src/scannererror.rs +++ b/escl-scan/src/scannererror.rs @@ -4,6 +4,7 @@ use core::fmt; pub enum ErrorCode { FilesystemError, NetworkError, + NoScannerFound, ProtocolError, ScannerNotReady, } @@ -19,6 +20,9 @@ impl fmt::Display for ScannerError { let msg = match self.code { ErrorCode::FilesystemError => format!("File System Error: {}", self.message), ErrorCode::NetworkError => format!("Network Error: {}", self.message), + ErrorCode::NoScannerFound => { + format!("No scanner found where name contains {}", self.message) + } ErrorCode::ProtocolError => format!("eSCL Protocol Error: {}", self.message), ErrorCode::ScannerNotReady => "The scanner is not ready to scan".to_string(), }; @@ -53,3 +57,12 @@ impl From for ScannerError { } } } + +impl From for ScannerError { + fn from(error: zeroconf::error::Error) -> Self { + ScannerError { + code: ErrorCode::NetworkError, + message: error.to_string(), + } + } +} diff --git a/escl-scan/src/scannerfinder.rs b/escl-scan/src/scannerfinder.rs new file mode 100644 index 0000000..978fd99 --- /dev/null +++ b/escl-scan/src/scannerfinder.rs @@ -0,0 +1,135 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::{ + any::Any, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +use zeroconf::{ + browser::TMdnsBrowser, event_loop::TEventLoop, txt_record::TTxtRecord, MdnsBrowser, + ServiceDiscovery, ServiceType, TxtRecord, +}; + +use crate::{ + scanner::Scanner, + scannererror::{ErrorCode, ScannerError}, +}; + +pub struct ScannerFinder { + scanners: Arc>>, +} + +impl ScannerFinder { + pub fn new() -> ScannerFinder { + //ScannerFinder { scanners: vec![] } + ScannerFinder { + scanners: Arc::new(Mutex::new(vec![])), + } + } + + fn scanner_found(&self, name: &str) -> bool { + let scanners = self.scanners.lock().unwrap(); + for scanner in scanners.iter() { + // TODO Search hostname and device name + if scanner.base_url.contains(name) { + return true; + } + } + + return false; + } + + pub fn find(&mut self, name: Option<&str>) -> Result, ScannerError> { + let service_type = + ServiceType::with_sub_types(&"uscan", &"tcp", vec![]).expect("invalid service type"); + log::info!("Looking for scanners with {service_type:?}"); + + let mut browser = MdnsBrowser::new(service_type); + browser.set_service_discovered_callback(Box::new(Self::on_service_discovered)); + + //let scanners: Arc>> = Arc::new(Mutex::new(vec![])); + browser.set_context(Box::new(Arc::clone(&self.scanners))); + + let event_loop = match browser.browse_services() { + Ok(event_loop) => event_loop, + Err(err) => return Err(err.into()), + }; + + let timeout = Duration::from_secs(5); + let end_time = Instant::now() + timeout; + while Instant::now() < end_time { + event_loop.poll(end_time - Instant::now()).unwrap(); + + if let Some(name) = name { + if self.scanner_found(name) { + log::info!("Found scanner for name {name}"); + return Ok(self.scanners.lock().unwrap().clone()); + } + } + } + + if let Some(name) = name { + log::info!("No scanner found for name {name}"); + return Err(ScannerError { + code: ErrorCode::NoScannerFound, + message: name.to_string(), + }); + } else { + let scanners = self.scanners.lock().unwrap(); + log::info!("Found {} scanners on the network", scanners.len()); + return Ok(scanners.clone()); + }; + } + + fn on_service_discovered( + result: zeroconf::Result, + context: Option>, + ) { + let service = match result { + Ok(service) => service, + Err(err) => { + log::info!("Error during scanner discovery (continuing): {err}"); + return; + } + }; + + log::info!("Service discovered: {service:?}",); + //let mut context = context.expect("We provided a scanner list as context"); + let scanners = context + .as_ref() + .unwrap() + .downcast_ref::>>>() + .unwrap(); + let mut scanners = scanners.lock().unwrap(); + + let txt: &TxtRecord = match service.txt() { + Some(txt) => txt, + None => { + log::warn!("Failed to get txt record for {service:?}"); + return; + } + }; + + let url_root = txt.get("rs"); + let device_name = match txt.get("ty") { + Some(name) => name, + None => { + log::warn!("Service has no human-readable device name: {service:?}"); + return; + } + }; + + let scanner = Scanner::new( + device_name.as_str(), + service.host_name(), + url_root.as_ref().map(|s| s.as_str()), + ); + log::info!("{:?}", scanner); + scanners.push(scanner); + } +}