use indexmap::IndexMap;
use std::collections::VecDeque;
use mumu::parser::types::Value;
use super::iterator::{handle_from_engine, LldpEngine};
use super::options::{LldpMode, LldpOptions};
use super::proto::DiscoveryProtocol;
use super::table::{Event, NeighborTable};
#[inline]
fn now_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn event_to_value(ev: Event) -> Value {
let mut base = match ev.row.to_value() {
Value::KeyedArray(m) => m,
other => {
let mut m = IndexMap::new();
m.insert("ok".into(), Value::Bool(false));
m.insert(
"message".into(),
Value::SingleString(format!("unexpected row type: {:?}", other)),
);
return Value::KeyedArray(m);
}
};
let mut out = IndexMap::new();
out.insert("ok".into(), Value::Bool(true));
out.insert(
"event".into(),
Value::SingleString(ev.kind.as_str().to_string()),
);
for (k, v) in base.drain(..) {
out.insert(k, v);
}
Value::KeyedArray(out)
}
#[cfg(target_os = "linux")]
mod real {
use super::*;
use super::super::capture::{CaptureError, RawCapture};
use super::super::parse::parse_any_row_from_frame;
struct CaptureCtx {
iface: String,
cap: RawCapture,
buf: Vec<u8>,
}
pub struct RealEngine {
caps: Vec<CaptureCtx>,
table: NeighborTable,
queue: VecDeque<Value>,
last_gc_ms: u64,
gc_interval_ms: u64,
coalesce_ms: u64,
remain: Option<usize>,
round: usize,
verbose: bool,
}
impl RealEngine {
fn new(opts: LldpOptions, caps: Vec<CaptureCtx>) -> Self {
let default_ttl_ms = (opts.ttl as u64).saturating_mul(1000).max(1000);
let table = NeighborTable::new(default_ttl_ms);
let gc_interval_ms = std::env::var("MUMU_LLDP_GC_MS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(1000);
let coalesce_ms = std::env::var("MUMU_LLDP_COALESCE_MS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(1500);
Self {
caps,
table,
queue: VecDeque::new(),
last_gc_ms: 0,
gc_interval_ms,
coalesce_ms,
remain: opts.count,
round: 0,
verbose: opts.verbose,
}
}
#[inline]
fn note_emitted_and_is_done(&mut self) -> bool {
if let Some(n) = self.remain {
if n > 0 {
self.remain = Some(n - 1);
}
}
matches!(self.remain, Some(0))
}
}
impl LldpEngine for RealEngine {
fn next(&mut self) -> Result<Value, String> {
if let Some(v) = self.queue.pop_front() {
let _done = self.note_emitted_and_is_done();
return Ok(v);
}
let caps_len = self.caps.len();
if caps_len > 0 {
let start = self.round % caps_len;
for i in 0..caps_len {
let idx = (start + i) % caps_len;
let ctx = &mut self.caps[idx];
match ctx.cap.recv(&mut ctx.buf) {
Ok(n) if n > 0 => {
let now = now_ms();
if let Some(row) =
parse_any_row_from_frame(&ctx.iface, &ctx.buf[..n], now)
{
if let Some(ev) = self
.table
.upsert_and_maybe_event(row, now, self.coalesce_ms)
{
let v = event_to_value(ev);
let _done = {
let is_done_after = self.note_emitted_and_is_done();
self.round = idx.wrapping_add(1);
is_done_after
};
return Ok(v);
}
}
self.round = idx.wrapping_add(1);
continue;
}
Err(CaptureError::NoData) => {
self.round = idx.wrapping_add(1);
continue;
}
Err(e) => {
if self.verbose {
eprintln!("[net:lldp] capture error on {} => {:?}", ctx.iface, e);
}
self.round = idx.wrapping_add(1);
continue;
}
_ => {
self.round = idx.wrapping_add(1);
continue;
}
}
}
}
let now = now_ms();
if now.saturating_sub(self.last_gc_ms) >= self.gc_interval_ms {
let removes = self.table.gc_expired_events(now);
if !removes.is_empty() {
for ev in removes {
self.queue.push_back(event_to_value(ev));
}
self.last_gc_ms = now;
if let Some(v) = self.queue.pop_front() {
let _done = self.note_emitted_and_is_done();
return Ok(v);
}
} else {
self.last_gc_ms = now;
}
}
if matches!(self.remain, Some(0)) {
return Err("NO_MORE_DATA".into());
}
Err("AGAIN".into())
}
}
pub fn spawn_iterator(opts: LldpOptions) -> mumu::parser::types::IteratorHandle {
let mut caps: Vec<CaptureCtx> = Vec::new();
let ifaces = {
let v = opts.effective_ifaces();
if v.is_empty() {
vec!["eth0".to_string()]
} else {
v
}
};
let want_lldp = opts
.protocols
.iter()
.any(|p| *p == DiscoveryProtocol::LLDP);
let want_cdp = opts
.protocols
.iter()
.any(|p| *p == DiscoveryProtocol::CDP);
for name in ifaces {
let opened = RawCapture::open_configured(Some(&name), opts.promisc, want_lldp, want_cdp);
match opened {
Ok(rc) => {
let buf_cap = (opts.snaplen.max(64) as usize).saturating_add(64);
let cap = CaptureCtx {
iface: name,
cap: rc,
buf: vec![0u8; buf_cap],
};
if opts.verbose {
eprintln!(
"[net:lldp] opened capture iface='{}' promisc={} snaplen={} buf={}",
cap.iface, opts.promisc, opts.snaplen, buf_cap
);
}
caps.push(cap);
}
Err(e) => {
if opts.verbose {
eprintln!("[net:lldp] capture open failed on {} => {:?}", name, e);
}
}
}
}
if caps.is_empty() {
let row = build_error_row_basic(
&opts,
"net:lldp — could not open any capture sockets (check permissions and interface names)",
);
return handle_from_engine(Box::new(ErrorOnceEngine { row: Some(row) }));
}
let eng = RealEngine::new(opts, caps);
handle_from_engine(Box::new(eng))
}
fn build_error_row_basic(opts: &LldpOptions, message: &str) -> Value {
let mut map: IndexMap<String, Value> = IndexMap::new();
map.insert("ok".into(), Value::Bool(false));
map.insert(
"message".into(),
Value::SingleString(message.to_string()),
);
let iface = if let Some(i) = &opts.iface {
i.clone()
} else if let Some(first) = opts.ifaces.get(0) {
first.clone()
} else {
"unknown".to_string()
};
map.insert("iface".into(), Value::SingleString(iface));
map.insert("protocol".into(), Value::SingleString("LLDP".into())); map.insert(
"mode".into(),
Value::SingleString(match opts.mode {
LldpMode::Listen => "listen".into(),
LldpMode::Advertise => "advertise".into(),
LldpMode::Discover => "discover".into(),
}),
);
Value::KeyedArray(map)
}
struct ErrorOnceEngine {
row: Option<Value>,
}
impl LldpEngine for ErrorOnceEngine {
fn next(&mut self) -> Result<Value, String> {
match self.row.take() {
Some(v) => Ok(v),
None => Err("NO_MORE_DATA".into()),
}
}
}
}
#[cfg(not(target_os = "linux"))]
mod real {
use super::*;
use crate::lldp::iterator::LldpEngine;
pub fn spawn_iterator(opts: LldpOptions) -> mumu::parser::types::IteratorHandle {
let row = build_error_row(&opts);
handle_from_engine(Box::new(ErrorOnceEngine { row: Some(row) }))
}
fn build_error_row(opts: &LldpOptions) -> Value {
let mut map: IndexMap<String, Value> = IndexMap::new();
map.insert("ok".into(), Value::Bool(false));
map.insert(
"message".into(),
Value::SingleString(
"net:lldp real engine is unavailable on this platform (Linux-only)".to_string(),
),
);
let iface = if let Some(i) = &opts.iface {
i.clone()
} else if let Some(first) = opts.ifaces.get(0) {
first.clone()
} else {
"unknown".to_string()
};
map.insert("iface".into(), Value::SingleString(iface));
map.insert("protocol".into(), Value::SingleString("LLDP".into()));
map.insert(
"mode".into(),
Value::SingleString(match opts.mode {
LldpMode::Listen => "listen".into(),
LldpMode::Advertise => "advertise".into(),
LldpMode::Discover => "discover".into(),
}),
);
Value::KeyedArray(map)
}
struct ErrorOnceEngine {
row: Option<Value>,
}
impl LldpEngine for ErrorOnceEngine {
fn next(&mut self) -> Result<Value, String> {
match self.row.take() {
Some(v) => Ok(v),
None => Err("NO_MORE_DATA".into()),
}
}
}
}
pub fn spawn_iterator(opts: LldpOptions) -> mumu::parser::types::IteratorHandle {
real::spawn_iterator(opts)
}