//! The task that tells the cloud what this gateway can see. //! //! Its reason for existing is small and specific: adding a printer in the web //! console should not begin with someone walking to a machine to read its IP //! address off the screen. It cannot finish the job - the credential is on //! that screen and no probe can read it - but it removes the address, the //! serial number and the guesswork about which vendor the thing is. //! //! # It does nothing unless asked //! //! `discovery.enabled` is false by default, and this task then parks forever //! rather than looping over a disabled check. The reason is in //! `DiscoverySettings`: a sweep is unsolicited traffic on someone else's //! network, and a gateway that did it by default would be scanning networks //! its operator never meant to point it at. //! //! Note which way the switch faces. It is read from the gateway's own config //! and the cloud cannot set it, because a setting whose entire purpose is to //! bound what this process does to the local network has to be decided where //! that network is understood. use std::time::Duration; use tokio::sync::mpsc::Sender; use crate::config::DiscoverySettings; use crate::uplink::ClientMessage; /// Scans on a timer and reports each pass, forever. /// /// Never returns, so it sits in `main`'s `select!` alongside the other /// long-running tasks. With discovery off it parks on a future that never /// completes, which costs nothing and keeps the "every arm runs forever" /// property of that `select!` true. pub async fn run(settings: DiscoverySettings, printers_path: String, outbound: Sender) { if !settings.enabled { tracing::info!("printer discovery is off (discovery.enabled); not scanning this network"); std::future::pending::<()>().await; return; } tracing::info!( subnets = ?settings.subnets, ssdp = settings.ssdp, serial = settings.serial, every = ?settings.interval, "printer discovery is ON; this gateway will probe the network above" ); let mut ticker = tokio::time::interval(settings.interval); // A pass takes tens of seconds - an SSDP window alone is 32 - so a tick // can come due while one is still running. `Delay` drops the ones that // came due rather than firing them back to back afterwards, which is what // `Burst` (the default) would do and would turn a slow scan into a // continuous one. ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { // Fires immediately the first time, so a gateway reports what it can // see shortly after boot rather than one interval later. ticker.tick().await; // Re-read every pass, not once at startup. Nothing re-reads the fleet // file on a running daemon yet, so adding a printer still means a // restart - but someone who has *edited* the file and not restarted // should not keep being offered the printer they just configured. let configured = match crate::fleet::configured(std::path::Path::new(&printers_path)) { Ok(configured) => configured, // Fail the pass rather than report against an empty set. The // alternative offers every printer the farm already runs as a // fresh find, which is worse than no report: it is a list of // things to add that are already added. Err(err) => { tracing::warn!(%err, "cannot tell which printers are already configured; skipping this discovery pass"); continue; } }; let scan = super::scan::scan(&settings, &configured).await; if !scan.looked() { // Every enabled source failed. An empty report would assert that // this network has no unconfigured printers, which is a claim // about the farm we have no evidence for - see `Scan::looked`. tracing::warn!( attempted = scan.attempted, "every discovery source failed; not reporting, because an empty report would mean 'nothing is there'" ); continue; } let printers: Vec<_> = scan.candidates.iter().map(super::Candidate::to_wire).collect(); tracing::info!(count = printers.len(), "reporting discovered printers"); let message = ClientMessage::DiscoveredPrinters { printers, scanned_at: now_rfc3339(), }; // `send`, not `try_send`: the queue outlives any one connection, so // waiting here is waiting for the cloud to come back, which is // exactly the right thing to do with a scan result. If the channel is // ever closed the uplink is gone and so is the process. if outbound.send(message).await.is_err() { tracing::warn!("uplink queue closed; stopping discovery reports"); return; } } } /// RFC 3339 in UTC, which is what the schema's `date-time` means. /// /// Public so `examples/discover_printers.rs` can print a frame with a real /// timestamp in it - a capture is meant to be feedable straight to the /// backend's validator, and a placeholder there would not be. pub fn now_rfc3339() -> String { let secs = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or(Duration::ZERO) .as_secs(); format_rfc3339(secs) } /// Formats a Unix second count, with the clock passed in so it is testable. /// /// Hand-rolled because this crate has no date library and one field does not /// justify adding one - `scanned_at` is read by a human looking at a "found /// 20 minutes ago" label, not by arithmetic. fn format_rfc3339(secs: u64) -> String { let (days, time) = (secs / 86_400, secs % 86_400); let (hour, minute, second) = (time / 3600, (time % 3600) / 60, time % 60); // Civil date from a day count, by the algorithm in Howard Hinnant's // `civil_from_days`: shift the epoch to 0000-03-01 so a leap day lands at // the end of the cycle and the month-length pattern repeats cleanly. let z = days as i64 + 719_468; let era = z.div_euclid(146_097); let doe = z.rem_euclid(146_097); let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1; let month = if mp < 10 { mp + 3 } else { mp - 9 }; let year = yoe + era * 400 + i64::from(month <= 2); format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z") } #[cfg(test)] mod tests { use super::*; #[test] fn timestamps_match_what_date_u_prints() { // Pinned against `date -u -d @N`: an off-by-one in the civil date // arithmetic is invisible until it is a day or a year out. assert_eq!(format_rfc3339(0), "1970-01-01T00:00:00Z"); // A leap day, which is the whole reason this arithmetic needs care. assert_eq!(format_rfc3339(1_709_164_800), "2024-02-29T00:00:00Z"); assert_eq!(format_rfc3339(1_792_413_296), "2026-10-19T12:34:56Z"); // Past 2100, where the "divisible by 100 is not a leap year" rule // bites and a naive every-four-years version drifts. assert_eq!(format_rfc3339(4_102_444_800), "2100-01-01T00:00:00Z"); // The last second of a year, which catches a December/January slip. assert_eq!(format_rfc3339(1_767_225_599), "2025-12-31T23:59:59Z"); } }