doc sweep!

This commit is contained in:
lda
2026-04-06 05:43:37 +07:00 Unverified
parent 67aa50ec5c
commit 79a6157a3a
22 changed files with 188 additions and 23 deletions
+21 -11
View File
@@ -1,8 +1,7 @@
//! ts //! Typed wrappers for `ip -j address show`.
//! //!
//! deals with both ip a (addroutput) and ip l (commonoutput) //! This module is intentionally close to the Linux output shape while still
//! //! tightening a few fields into more useful Rust types.
//! lowk why its free but its indirection and its ass
pub mod json; pub mod json;
#[cfg(all(unix, feature = "experimental-nl"))] #[cfg(all(unix, feature = "experimental-nl"))]
@@ -16,22 +15,21 @@ use std::net::{IpAddr, Ipv4Addr};
use crate::subcommands::link::OperState; use crate::subcommands::link::OperState;
/// i dont include what i dont know about (almost all ts) /// One interface row from `ip -j address show`.
#[derive(Serialize, Debug, Deserialize)] #[derive(Serialize, Debug, Deserialize)]
pub struct AddrOutput { pub struct AddrOutput {
pub ifindex: u32, pub ifindex: u32,
pub ifname: String, pub ifname: String,
/// i imagine UP or DOWN, unknown /// Interface operational state.
pub operstate: OperState, pub operstate: OperState,
// 6 has a serde and the enum doesnt? why. (serializing ts is ass although... im not given an array. they string formatted ts)
#[serde(with = "option_mac", default)] #[serde(with = "option_mac", default)]
pub address: Option<MacAddr>, pub address: Option<MacAddr>,
#[serde(default)] // i wish we have intellisense for this... fuck you metaprogramming /// Per-address entries attached to this interface.
#[serde(default)]
pub addr_info: Vec<AddrInfo>, pub addr_info: Vec<AddrInfo>,
} }
// i be copying /// One address entry nested under an interface row.
// Raw JSON shape from ip -j -4 address show
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct AddrInfo { pub struct AddrInfo {
pub family: Option<AddressFamily>, pub family: Option<AddressFamily>,
@@ -43,9 +41,9 @@ pub struct AddrInfo {
pub scope: Option<String>, pub scope: Option<String>,
pub label: Option<String>, pub label: Option<String>,
// many more exist; we only take what we need
} }
/// Address family used by `ip address` output.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressFamily { pub enum AddressFamily {
Inet, Inet,
@@ -90,6 +88,10 @@ impl<'de> Deserialize<'de> for AddressFamily {
} }
} }
/// Raw parsed local address plus prefix length.
///
/// This is still a source-shaped type; callers that need a guaranteed usable
/// CIDR should validate that both fields are present.
#[derive(Debug, Default, Clone, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct InterfaceCidr { pub struct InterfaceCidr {
pub local: Option<IpAddr>, pub local: Option<IpAddr>,
@@ -103,37 +105,45 @@ impl InterfaceCidr {
} }
impl AddrInfo { impl AddrInfo {
/// Return the parsed local IP address when present.
pub fn local_addr(&self) -> Option<IpAddr> { pub fn local_addr(&self) -> Option<IpAddr> {
self.cidr.local self.cidr.local
} }
/// Return the parsed prefix length when present.
pub fn prefixlen(&self) -> Option<u8> { pub fn prefixlen(&self) -> Option<u8> {
self.cidr.prefixlen self.cidr.prefixlen
} }
/// Return whether this row is IPv4.
pub fn is_ipv4(&self) -> bool { pub fn is_ipv4(&self) -> bool {
matches!(self.family, Some(AddressFamily::Inet)) matches!(self.family, Some(AddressFamily::Inet))
} }
/// Return whether this row is IPv6.
pub fn is_ipv6(&self) -> bool { pub fn is_ipv6(&self) -> bool {
matches!(self.family, Some(AddressFamily::Inet6)) matches!(self.family, Some(AddressFamily::Inet6))
} }
} }
impl AddrOutput { impl AddrOutput {
/// Iterate IPv4 address entries.
pub fn ipv4_addrs(&self) -> impl Iterator<Item = &AddrInfo> { pub fn ipv4_addrs(&self) -> impl Iterator<Item = &AddrInfo> {
self.addr_info.iter().filter(|info| info.is_ipv4()) self.addr_info.iter().filter(|info| info.is_ipv4())
} }
/// Iterate IPv6 address entries.
pub fn ipv6_addrs(&self) -> impl Iterator<Item = &AddrInfo> { pub fn ipv6_addrs(&self) -> impl Iterator<Item = &AddrInfo> {
self.addr_info.iter().filter(|info| info.is_ipv6()) self.addr_info.iter().filter(|info| info.is_ipv6())
} }
} }
/// Fetch address data using the default backend.
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> { pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
get_with_backend(Backend::Json, dev).await get_with_backend(Backend::Json, dev).await
} }
/// Fetch address data using an explicit backend.
pub async fn get_with_backend( pub async fn get_with_backend(
backend: Backend, backend: Backend,
dev: Option<&str>, dev: Option<&str>,
+4
View File
@@ -11,6 +11,7 @@ use macaddr::MacAddr;
use rtnetlink::packet_route::link::State as NetlinkOperState; use rtnetlink::packet_route::link::State as NetlinkOperState;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// One interface row from `ip -j link show`.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkOutput { pub struct LinkOutput {
pub ifindex: u32, pub ifindex: u32,
@@ -21,6 +22,7 @@ pub struct LinkOutput {
pub address: Option<MacAddr>, pub address: Option<MacAddr>,
} }
/// Operational state of a Linux network interface.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperState { pub enum OperState {
Up, Up,
@@ -100,10 +102,12 @@ impl From<NetlinkOperState> for OperState {
} }
} }
/// Fetch link rows using the default backend.
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> { pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
get_with_backend(Backend::Json, dev).await get_with_backend(Backend::Json, dev).await
} }
/// Fetch link rows using an explicit backend.
pub async fn get_with_backend( pub async fn get_with_backend(
backend: Backend, backend: Backend,
dev: Option<&str>, dev: Option<&str>,
+10 -12
View File
@@ -1,8 +1,4 @@
//! ```bash //! Typed wrappers for `ip -j neigh show`.
//! ip -j n s
//! ```
//!
//! yes. this is a real call.
pub mod json; pub mod json;
#[cfg(all(unix, feature = "experimental-nl"))] #[cfg(all(unix, feature = "experimental-nl"))]
@@ -16,18 +12,18 @@ use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{Display, EnumString}; use strum::{Display, EnumString};
/// Structured neighbor query input matching the common `ip neigh` flags.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborInput { pub struct NeighborInput {
/// supports only the last item (ignore), `to` keyword is optional /// Destination address filter. The optional `to` keyword in CLI form is implicit here.
pub to: Option<IpAddr>, pub to: Option<IpAddr>,
/// supports only one item (it complains if multiple) /// Interface-name filter.
pub dev: Option<String>, // im all for simplicity pub dev: Option<String>,
/// takes multiple, has to have `nud` before bro or it will think you `to` /// Neighbor-state filters.
pub nud: Vec<NUDState>, pub nud: Vec<NUDState>,
} }
// as input this must be lowercase. as output it is uppercase /// Linux neighbor reachability states.
/// docs for items come from a random ahh man website idk
#[derive( #[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default, Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
)] )]
@@ -70,7 +66,7 @@ pub enum NUDState {
Other(u16), Other(u16),
} }
/// everything i see /// One neighbor row from `ip -j neigh show`.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborItem { pub struct NeighborItem {
#[serde(rename(deserialize = "dst"))] #[serde(rename(deserialize = "dst"))]
@@ -83,6 +79,7 @@ pub struct NeighborItem {
pub state: Vec<NUDState>, pub state: Vec<NUDState>,
} }
/// Fetch neighbor rows using the default backend.
pub async fn get( pub async fn get(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
@@ -91,6 +88,7 @@ pub async fn get(
get_with_backend(Backend::Json, ip, dev, nud).await get_with_backend(Backend::Json, ip, dev, nud).await
} }
/// Fetch neighbor rows using an explicit backend.
pub async fn get_with_backend( pub async fn get_with_backend(
backend: Backend, backend: Backend,
ip: Option<IpAddr>, ip: Option<IpAddr>,
+13
View File
@@ -5,6 +5,7 @@ use wakey_core::{
WakeTargetResult, WakeTargetResult,
}; };
/// Legacy status row shape expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct LegacyStatusRow { pub struct LegacyStatusRow {
pub ip: std::net::IpAddr, pub ip: std::net::IpAddr,
@@ -14,6 +15,7 @@ pub struct LegacyStatusRow {
pub state: wakey_core::NeighborState, pub state: wakey_core::NeighborState,
} }
/// Legacy status response shape expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct LegacyStatusResponse { pub struct LegacyStatusResponse {
pub name: Option<String>, pub name: Option<String>,
@@ -21,6 +23,7 @@ pub struct LegacyStatusResponse {
pub filters: DeviceFilters, pub filters: DeviceFilters,
} }
/// Legacy DHCP lease row shape expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct LegacyLeaseRow { pub struct LegacyLeaseRow {
pub expires_epoch: u64, pub expires_epoch: u64,
@@ -31,11 +34,13 @@ pub struct LegacyLeaseRow {
pub nud_state: Option<wakey_core::NeighborState>, pub nud_state: Option<wakey_core::NeighborState>,
} }
/// Legacy wake response wrapper expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct LegacyWakeResult { pub struct LegacyWakeResult {
pub result: Vec<LegacyWakeResultRow>, pub result: Vec<LegacyWakeResultRow>,
} }
/// Legacy per-target wake row shape.
#[derive(Debug, Clone, Copy, Serialize)] #[derive(Debug, Clone, Copy, Serialize)]
pub struct LegacyWakeResultRow { pub struct LegacyWakeResultRow {
#[serde(flatten)] #[serde(flatten)]
@@ -43,6 +48,7 @@ pub struct LegacyWakeResultRow {
pub status: wakey_core::WakeStatus, pub status: wakey_core::WakeStatus,
} }
/// Map legacy-style status rows into the old response shape.
pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusResponse { pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusResponse {
LegacyStatusResponse { LegacyStatusResponse {
name: status.name, name: status.name,
@@ -51,6 +57,7 @@ pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusR
} }
} }
/// Project a device inventory into the legacy status response shape.
pub fn legacy_status_from_inventory( pub fn legacy_status_from_inventory(
inventory: DeviceInventory, inventory: DeviceInventory,
name: Option<String>, name: Option<String>,
@@ -68,6 +75,7 @@ pub fn legacy_status_from_inventory(
} }
} }
/// Convert one neighbor row to the legacy status row shape.
pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow { pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
LegacyStatusRow { LegacyStatusRow {
ip: row.ip, ip: row.ip,
@@ -77,6 +85,7 @@ pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
} }
} }
/// Project one merged device back into legacy status rows.
pub fn legacy_status_rows_from_device(device: Device) -> Vec<LegacyStatusRow> { pub fn legacy_status_rows_from_device(device: Device) -> Vec<LegacyStatusRow> {
if !device.neighbors.is_empty() { if !device.neighbors.is_empty() {
return device return device
@@ -107,10 +116,12 @@ pub fn legacy_status_rows_from_device(device: Device) -> Vec<LegacyStatusRow> {
.collect() .collect()
} }
/// Convert lease rows into the legacy frontend shape.
pub fn legacy_leases_from_domain(leases: Vec<DhcpLeaseWithState>) -> Vec<LegacyLeaseRow> { pub fn legacy_leases_from_domain(leases: Vec<DhcpLeaseWithState>) -> Vec<LegacyLeaseRow> {
leases.into_iter().map(legacy_lease_row).collect() leases.into_iter().map(legacy_lease_row).collect()
} }
/// Convert one lease row into the legacy frontend shape.
pub fn legacy_lease_row(lease: DhcpLeaseWithState) -> LegacyLeaseRow { pub fn legacy_lease_row(lease: DhcpLeaseWithState) -> LegacyLeaseRow {
LegacyLeaseRow { LegacyLeaseRow {
expires_epoch: lease.lease_line.expires_epoch, expires_epoch: lease.lease_line.expires_epoch,
@@ -121,12 +132,14 @@ pub fn legacy_lease_row(lease: DhcpLeaseWithState) -> LegacyLeaseRow {
} }
} }
/// Convert wake results into the legacy frontend shape.
pub fn legacy_wake_from_domain(result: WakeResult) -> LegacyWakeResult { pub fn legacy_wake_from_domain(result: WakeResult) -> LegacyWakeResult {
LegacyWakeResult { LegacyWakeResult {
result: result.result.into_iter().map(legacy_wake_row).collect(), result: result.result.into_iter().map(legacy_wake_row).collect(),
} }
} }
/// Convert one wake result row into the legacy frontend shape.
pub fn legacy_wake_row(row: WakeTargetResult) -> LegacyWakeResultRow { pub fn legacy_wake_row(row: WakeTargetResult) -> LegacyWakeResultRow {
LegacyWakeResultRow { LegacyWakeResultRow {
target: row.target, target: row.target,
+3
View File
@@ -7,6 +7,7 @@ use axum::Router;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
/// Build the temporary HTTP app that serves the legacy API and static frontend.
pub fn http_app(static_root: std::path::PathBuf) -> Router { pub fn http_app(static_root: std::path::PathBuf) -> Router {
Router::new() Router::new()
.nest("/api", route::api_router()) .nest("/api", route::api_router())
@@ -20,11 +21,13 @@ pub fn http_app(static_root: std::path::PathBuf) -> Router {
)) ))
} }
/// Serve the temporary HTTP app on the provided socket address.
pub async fn serve_http(addr: SocketAddr, static_root: std::path::PathBuf) -> io::Result<()> { pub async fn serve_http(addr: SocketAddr, static_root: std::path::PathBuf) -> io::Result<()> {
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
axum::serve(listener, http_app(static_root).into_make_service()).await axum::serve(listener, http_app(static_root).into_make_service()).await
} }
/// Serve the HTTP app using the `static/` directory next to the current executable.
pub async fn serve_http_from_current_exe(addr: SocketAddr) -> io::Result<()> { pub async fn serve_http_from_current_exe(addr: SocketAddr) -> io::Result<()> {
let exe = std::env::current_exe()?; let exe = std::env::current_exe()?;
let root = exe let root = exe
+6
View File
@@ -1,14 +1,19 @@
use anyhow::Result; use anyhow::Result;
use wakey_core::InterfaceSummary; use wakey_core::InterfaceSummary;
/// Return interface names only.
///
/// This is the old, lightweight interface listing surface kept for compatibility.
pub async fn list_interfaces() -> Result<Vec<String>> { pub async fn list_interfaces() -> Result<Vec<String>> {
Ok(wakey_linux::devices::devs_sorted().await) Ok(wakey_linux::devices::devs_sorted().await)
} }
/// Return condensed interface summaries useful for CLI and wake routing.
pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> { pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> {
wakey_linux::devices::list_interface_summaries().await wakey_linux::devices::list_interface_summaries().await
} }
/// Return one named interface summary when present.
pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary>> { pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary>> {
Ok(get_interface_summaries() Ok(get_interface_summaries()
.await? .await?
@@ -16,6 +21,7 @@ pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary
.find(|iface| iface.ifname == name)) .find(|iface| iface.ifname == name))
} }
/// Resolve a hostname through the local resolver and collect all returned IPs.
pub async fn get_ips(name: impl AsRef<str>) -> Result<Vec<std::net::IpAddr>> { pub async fn get_ips(name: impl AsRef<str>) -> Result<Vec<std::net::IpAddr>> {
Ok(wakey_linux::devices::get_ips(name.as_ref()) Ok(wakey_linux::devices::get_ips(name.as_ref())
.await? .await?
+10
View File
@@ -6,11 +6,17 @@ use wakey_core::{
use crate::service::leases::get_leases; use crate::service::leases::get_leases;
use crate::service::query::resolve_query; use crate::service::query::resolve_query;
/// Resolve free-form input and return merged devices rather than raw source rows.
pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> { pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
let query = resolve_query(input).await?; let query = resolve_query(input).await?;
inventory(query).await.map(|inventory| inventory.devices) inventory(query).await.map(|inventory| inventory.devices)
} }
/// Build a merged device inventory from neighbor-table and DHCP-lease sources.
///
/// This is the current center of gravity for the service layer. Higher-level
/// status and wake flows should prefer deriving from inventory rather than
/// directly from raw Linux source rows.
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> { pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
let neighbors = wakey_linux::devices::query_status(&query).await?; let neighbors = wakey_linux::devices::query_status(&query).await?;
let leases = get_leases(wakey_core::LeaseQuery { let leases = get_leases(wakey_core::LeaseQuery {
@@ -22,6 +28,10 @@ pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
}) })
} }
/// Merge raw neighbor entries and DHCP leases into device aggregates.
///
/// Identity is currently MAC-first, with an IP-based fallback when a neighbor
/// row does not include a MAC address.
pub fn merge_devices( pub fn merge_devices(
neighbors: Vec<NeighborEntry>, neighbors: Vec<NeighborEntry>,
leases: Vec<DhcpLeaseWithState>, leases: Vec<DhcpLeaseWithState>,
+2
View File
@@ -1,6 +1,7 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use wakey_core::{DhcpLease, DhcpLeaseWithState, LeaseQuery}; use wakey_core::{DhcpLease, DhcpLeaseWithState, LeaseQuery};
/// Read DHCP leases and optionally enrich them with current neighbor-state data.
pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> { pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names() let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
.await .await
@@ -12,6 +13,7 @@ pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
} }
} }
/// Wrap raw DHCP leases in the current service output shape without neighbor state.
pub fn leases_without_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> { pub fn leases_without_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
leases leases
.into_iter() .into_iter()
+12
View File
@@ -1,10 +1,18 @@
use anyhow::Result; use anyhow::Result;
use wakey_core::{DeviceFilters, DeviceQuery, Query, QueryInput}; use wakey_core::{DeviceFilters, DeviceQuery, Query, QueryInput};
/// Resolve free-form user input into the legacy `DeviceQuery` filter shape.
///
/// This is the compatibility entrypoint used by CLI and HTTP paths that still
/// speak in terms of `DeviceQuery`.
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> { pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
query_to_device_query(resolve_selector(input).await?) query_to_device_query(resolve_selector(input).await?)
} }
/// Classify one piece of free-form user input into a typed selector.
///
/// The Linux adapter decides whether the input looks like an IP address, MAC,
/// interface name, neighbor state, or plain text.
pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> { pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
Ok( Ok(
match wakey_linux::devices::classify_query(input.into()).await { match wakey_linux::devices::classify_query(input.into()).await {
@@ -17,6 +25,10 @@ pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
) )
} }
/// Convert the newer selector-oriented `Query` model into a `DeviceQuery`.
///
/// This keeps the old filter-based service and HTTP surfaces working while the
/// internals migrate toward selector- and device-oriented APIs.
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> { pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
Ok(match query { Ok(match query {
Query::Ip(ip_addr) => DeviceQuery { Query::Ip(ip_addr) => DeviceQuery {
+10
View File
@@ -4,8 +4,13 @@ use wakey_core::{Device, DeviceQuery, NeighborEntry, Presence, Status};
use crate::service::inventory::inventory; use crate::service::inventory::inventory;
use crate::service::query::resolve_query; use crate::service::query::resolve_query;
/// Service status payload, still expressed in terms of legacy neighbor rows.
pub type StatusResponse = Status<NeighborEntry>; pub type StatusResponse = Status<NeighborEntry>;
/// Return status rows derived from the merged device inventory.
///
/// This keeps the old status response shape alive while the underlying model is
/// increasingly device-centered.
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> { pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
let inventory = inventory(query.clone()).await?; let inventory = inventory(query.clone()).await?;
let table = inventory let table = inventory
@@ -20,11 +25,16 @@ pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
}) })
} }
/// Convenience wrapper around [`get_status`] for free-form user input.
pub async fn get_status_for_input(input: impl Into<String>) -> Result<StatusResponse> { pub async fn get_status_for_input(input: impl Into<String>) -> Result<StatusResponse> {
let query = resolve_query(input).await?; let query = resolve_query(input).await?;
get_status(query).await get_status(query).await
} }
/// Project a device aggregate back into legacy status rows.
///
/// If the device already has neighbor rows they are reused directly; otherwise a
/// fallback row is synthesized from the best available device data.
pub fn device_to_status_rows(device: &Device) -> Vec<NeighborEntry> { pub fn device_to_status_rows(device: &Device) -> Vec<NeighborEntry> {
if !device.neighbors.is_empty() { if !device.neighbors.is_empty() {
return device.neighbors.clone(); return device.neighbors.clone();
+10
View File
@@ -6,6 +6,7 @@ use wakey_core::{WakeResult, WakeTarget};
use crate::service::interfaces::get_interface_summaries; use crate::service::interfaces::get_interface_summaries;
use crate::service::inventory::resolve_devices; use crate::service::inventory::resolve_devices;
/// Send Wake-on-LAN packets for already-concrete wake targets.
pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> { pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> {
let result = wakey_linux::wake::wake_many(targets) let result = wakey_linux::wake::wake_many(targets)
.await .await
@@ -13,11 +14,15 @@ pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> {
Ok(WakeResult { result }) Ok(WakeResult { result })
} }
/// Resolve free-form input into wake targets and send the packets.
pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> { pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> {
let targets = resolve_wake_targets(input).await?; let targets = resolve_wake_targets(input).await?;
wake_targets(targets).await wake_targets(targets).await
} }
/// Build broadcast wake targets for every broadcast-capable interface.
///
/// This is used by explicit manual wake mode when only a MAC address is supplied.
pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> { pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
Ok(get_interface_summaries() Ok(get_interface_summaries()
.await? .await?
@@ -31,6 +36,7 @@ pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
.collect()) .collect())
} }
/// Wake a device explicitly by MAC, optionally targeting a specific IP/broadcast.
pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> { pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> {
let targets = match ip { let targets = match ip {
Some(ip) => vec![WakeTarget { Some(ip) => vec![WakeTarget {
@@ -42,6 +48,10 @@ pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResul
wake_targets(targets).await wake_targets(targets).await
} }
/// Resolve free-form input into concrete wake targets.
///
/// The current resolution strategy fans out one wake target per resolved device IP,
/// using the first known MAC address for that device.
pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTarget>> { pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTarget>> {
let devices = resolve_devices(input).await?; let devices = resolve_devices(input).await?;
Ok(devices Ok(devices
+8
View File
@@ -6,6 +6,7 @@ use std::net::IpAddr;
use crate::model::{DhcpLease, NeighborEntry, NeighborState}; use crate::model::{DhcpLease, NeighborEntry, NeighborState};
use crate::parse::mac; use crate::parse::mac;
/// Product-level presence derived from raw neighbor state.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, serde::Deserialize, Default)] #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, serde::Deserialize, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Presence { pub enum Presence {
@@ -31,12 +32,17 @@ impl From<NeighborState> for Presence {
} }
} }
/// MAC-first identifier for a device aggregate.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
pub struct DeviceId { pub struct DeviceId {
#[serde(with = "mac")] #[serde(with = "mac")]
pub mac: MacAddr, pub mac: MacAddr,
} }
/// Merged view of one discovered network identity.
///
/// This aggregates facts from DHCP leases and neighbor-table rows into a more
/// useful application-level shape.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Device { pub struct Device {
@@ -52,6 +58,7 @@ pub struct Device {
} }
impl Device { impl Device {
/// Merge raw neighbor and DHCP facts into one device aggregate.
pub fn from_parts(neighbors: Vec<NeighborEntry>, leases: Vec<DhcpLease>) -> Self { pub fn from_parts(neighbors: Vec<NeighborEntry>, leases: Vec<DhcpLease>) -> Self {
use std::collections::BTreeSet; use std::collections::BTreeSet;
@@ -117,6 +124,7 @@ impl From<u8> for Presence {
} }
} }
/// Collection of merged discovered devices.
#[derive(Debug, Default, Clone, Serialize)] #[derive(Debug, Default, Clone, Serialize)]
pub struct DeviceInventory { pub struct DeviceInventory {
pub devices: Vec<Device>, pub devices: Vec<Device>,
+3
View File
@@ -6,6 +6,7 @@ use std::net::IpAddr;
use crate::model::NeighborState; use crate::model::NeighborState;
use crate::parse::mac; use crate::parse::mac;
/// One parsed DHCP lease row.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct DhcpLease { pub struct DhcpLease {
pub expires_epoch: u64, pub expires_epoch: u64,
@@ -15,6 +16,7 @@ pub struct DhcpLease {
pub name: Option<String>, pub name: Option<String>,
} }
/// DHCP lease row plus optional current neighbor-state enrichment.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct DhcpLeaseWithState { pub struct DhcpLeaseWithState {
@@ -23,6 +25,7 @@ pub struct DhcpLeaseWithState {
pub nud_state: Option<NeighborState>, pub nud_state: Option<NeighborState>,
} }
/// Options for lease retrieval from the service layer.
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct LeaseQuery { pub struct LeaseQuery {
pub include_state: bool, pub include_state: bool,
+17
View File
@@ -4,23 +4,40 @@ use serde_with::skip_serializing_none;
use crate::parse::mac; use crate::parse::mac;
/// A condensed, operator-oriented view of one network interface.
///
/// This is intentionally smaller than the full Linux `ip address show` / `ip link show`
/// payload. It keeps the fields that are currently useful to `wakey`:
/// interface identity, operational state, MAC address, bound addresses, and
/// IPv4 broadcast targets.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct InterfaceSummary { pub struct InterfaceSummary {
/// Kernel interface index.
pub ifindex: u32, pub ifindex: u32,
/// Interface name such as `br-lan`, `eth0`, or `wlan0`.
pub ifname: String, pub ifname: String,
/// Lowercased operational state such as `up`, `down`, or `unknown`.
pub operstate: String, pub operstate: String,
#[serde(with = "mac::option_mac")] #[serde(with = "mac::option_mac")]
/// Link-layer address when one exists.
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
/// Interface-bound addresses projected into a smaller usable shape.
pub addrs: Vec<InterfaceAddr>, pub addrs: Vec<InterfaceAddr>,
} }
/// A condensed view of one bound interface address.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct InterfaceAddr { pub struct InterfaceAddr {
/// Address family such as `inet` or `inet6`.
pub family: Option<String>, pub family: Option<String>,
/// CIDR notation such as `192.168.1.1/24`.
pub cidr: Option<String>, pub cidr: Option<String>,
/// IPv4 broadcast target when Linux reports one.
pub broadcast: Option<std::net::Ipv4Addr>, pub broadcast: Option<std::net::Ipv4Addr>,
/// Linux-reported address scope, for example `global` or `link`.
pub scope: Option<String>, pub scope: Option<String>,
/// Optional Linux label for the address entry.
pub label: Option<String>, pub label: Option<String>,
} }
+6
View File
@@ -6,6 +6,7 @@ use strum::{Display, EnumString};
use crate::parse::mac; use crate::parse::mac;
/// One neighbor-table row, typically derived from `ip neigh` or netlink.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
pub struct NeighborEntry { pub struct NeighborEntry {
@@ -16,6 +17,7 @@ pub struct NeighborEntry {
pub state: NeighborState, pub state: NeighborState,
} }
/// Linux neighbor reachability state.
#[derive( #[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default, Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
)] )]
@@ -36,6 +38,7 @@ pub enum NeighborState {
} }
impl NeighborState { impl NeighborState {
/// Lowercase CLI argument form used by `ip neigh`.
pub const fn as_ip_neigh_arg(self) -> &'static str { pub const fn as_ip_neigh_arg(self) -> &'static str {
match self { match self {
NeighborState::Permanent => "permanent", NeighborState::Permanent => "permanent",
@@ -50,6 +53,7 @@ impl NeighborState {
} }
} }
/// Ordering rank used when choosing the “best” state among multiple rows.
pub const fn rank(self) -> u8 { pub const fn rank(self) -> u8 {
match self { match self {
NeighborState::Permanent | NeighborState::Reachable => 5, NeighborState::Permanent | NeighborState::Reachable => 5,
@@ -74,6 +78,7 @@ impl Ord for NeighborState {
} }
} }
/// Errors returned when parsing a text `ip neigh` line.
#[derive(Debug, Display, thiserror::Error)] #[derive(Debug, Display, thiserror::Error)]
pub enum NeighborParseError { pub enum NeighborParseError {
IpWhere, IpWhere,
@@ -83,6 +88,7 @@ pub enum NeighborParseError {
StateParseError(#[from] strum::ParseError), StateParseError(#[from] strum::ParseError),
} }
/// Parse one textual `ip neigh` line into a typed neighbor row.
pub fn parse_neighbor_line(s: &str) -> Result<NeighborEntry, NeighborParseError> { pub fn parse_neighbor_line(s: &str) -> Result<NeighborEntry, NeighborParseError> {
let mut it = s.split_whitespace(); let mut it = s.split_whitespace();
let ip: IpAddr = it.next().ok_or(NeighborParseError::IpWhere)?.parse()?; let ip: IpAddr = it.next().ok_or(NeighborParseError::IpWhere)?.parse()?;
+8
View File
@@ -5,6 +5,10 @@ use std::net::IpAddr;
use crate::model::NeighborState; use crate::model::NeighborState;
/// Legacy-compatible query shape used by HTTP and service adapters.
///
/// `name` carries free-form text selection, while `filter` carries explicit
/// machine-readable filters such as IPs, MACs, interfaces, and neighbor states.
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery { pub struct DeviceQuery {
pub name: Option<String>, pub name: Option<String>,
@@ -12,6 +16,7 @@ pub struct DeviceQuery {
pub filter: DeviceFilters, pub filter: DeviceFilters,
} }
/// Explicit device filters for source- and service-level queries.
#[serde_as] #[serde_as]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct DeviceFilters { pub struct DeviceFilters {
@@ -29,11 +34,13 @@ pub struct DeviceFilters {
pub macs: Vec<MacAddr>, pub macs: Vec<MacAddr>,
} }
/// Path helper for routes that receive a single `{name}` segment.
#[derive(Debug, Default, Clone, Hash, Deserialize)] #[derive(Debug, Default, Clone, Hash, Deserialize)]
pub struct NamePath { pub struct NamePath {
pub name: String, pub name: String,
} }
/// Low-level classified input used by Linux query classification.
#[derive(Debug)] #[derive(Debug)]
pub enum QueryInput { pub enum QueryInput {
Ip(IpAddr), Ip(IpAddr),
@@ -43,6 +50,7 @@ pub enum QueryInput {
Name(String), Name(String),
} }
/// Higher-level typed selector used by the service layer.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Query { pub enum Query {
Text(String), Text(String),
+8
View File
@@ -5,6 +5,9 @@ use std::net::IpAddr;
use crate::parse::mac; use crate::parse::mac;
/// Concrete Wake-on-LAN destination fields.
///
/// A fully usable target needs both an IP address and a MAC address.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq)]
pub struct WakeTarget { pub struct WakeTarget {
@@ -15,6 +18,7 @@ pub struct WakeTarget {
} }
impl WakeTarget { impl WakeTarget {
/// Return whether this target has both fields needed to send WoL.
pub const fn is_complete(&self) -> bool { pub const fn is_complete(&self) -> bool {
matches!( matches!(
self, self,
@@ -26,11 +30,13 @@ impl WakeTarget {
} }
} }
/// Result of waking one or more targets.
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct WakeResult { pub struct WakeResult {
pub result: Vec<WakeTargetResult>, pub result: Vec<WakeTargetResult>,
} }
/// Per-target wake result row.
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)] #[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
@@ -39,6 +45,7 @@ pub struct WakeTargetResult {
pub status: WakeStatus, pub status: WakeStatus,
} }
/// Outcome of trying to wake one target.
#[derive(Debug, Serialize, Clone, Copy, Hash, PartialEq, Eq)] #[derive(Debug, Serialize, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WakeStatus { pub enum WakeStatus {
@@ -49,6 +56,7 @@ pub enum WakeStatus {
} }
impl WakeTargetResult { impl WakeTargetResult {
/// Construct an incomplete result for a target missing required fields.
pub const fn incomplete(target: WakeTarget) -> Self { pub const fn incomplete(target: WakeTarget) -> Self {
Self { Self {
target, target,
+14
View File
@@ -3,6 +3,10 @@ use lda_ipjs::subcommands::address;
use std::collections::HashSet; use std::collections::HashSet;
use wakey_core::{InterfaceAddr, InterfaceSummary}; use wakey_core::{InterfaceAddr, InterfaceSummary};
/// Discover interface names from Linux without requiring `ip`.
///
/// This is the lowest-common-denominator interface listing path and is used for
/// quick existence checks and legacy string-only callers.
pub async fn list_devs() -> HashSet<String> { pub async fn list_devs() -> HashSet<String> {
fn get_dev() -> HashSet<String> { fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new(); let mut devs: HashSet<String> = HashSet::new();
@@ -50,6 +54,15 @@ pub async fn devs_sorted() -> Vec<String> {
v v
} }
/// Build condensed interface summaries from Linux address data.
///
/// On Unix this prefers the `ipjs` netlink-backed address path; elsewhere it
/// falls back to the JSON command path. The result is not a full `ip address show`
/// dump. It is a smaller projection containing the parts `wakey` currently uses:
/// interface name/index, operstate, MAC, bound addresses, and IPv4 broadcast
/// addresses for Wake-on-LAN delivery.
///
/// Loopback is intentionally excluded.
pub async fn list_interface_summaries() -> Result<Vec<InterfaceSummary>> { pub async fn list_interface_summaries() -> Result<Vec<InterfaceSummary>> {
#[cfg(unix)] #[cfg(unix)]
let rows = address::nl::get(None) let rows = address::nl::get(None)
@@ -91,6 +104,7 @@ pub async fn list_interface_summaries() -> Result<Vec<InterfaceSummary>> {
Ok(out) Ok(out)
} }
/// Return whether a named interface exists according to [`list_devs`].
pub async fn has_dev(name: &str) -> bool { pub async fn has_dev(name: &str) -> bool {
list_devs().await.contains(name) list_devs().await.contains(name)
} }
+7
View File
@@ -5,6 +5,7 @@ use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
use wakey_core::{DeviceQuery, NeighborEntry, NeighborState}; use wakey_core::{DeviceQuery, NeighborEntry, NeighborState};
/// Resolve a hostname through the local resolver and return all reported IPs.
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> { pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0)) Ok(tokio::net::lookup_host((machine_name, 0))
.await .await
@@ -12,6 +13,11 @@ pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>>
.map(|c| c.ip())) .map(|c| c.ip()))
} }
/// Query Linux neighbor data and project it into `wakey-core` neighbor rows.
///
/// `machine_names` are resolved first and intersected with any explicit `ips`
/// filter. On Unix this prefers the netlink-backed `ipjs` path; elsewhere it
/// falls back to the JSON command backend.
pub async fn get_neighbors( pub async fn get_neighbors(
machine_names: &[impl AsRef<str>], machine_names: &[impl AsRef<str>],
ips: &[IpAddr], ips: &[IpAddr],
@@ -100,6 +106,7 @@ pub async fn get_neighbors(
} }
} }
/// Convenience wrapper around [`get_neighbors`] using the legacy `DeviceQuery`.
pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> { pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> {
get_neighbors( get_neighbors(
query.name.as_slice(), query.name.as_slice(),
+4
View File
@@ -4,6 +4,10 @@ use wakey_core::{NeighborState, QueryInput, parse};
use crate::devices::interfaces::has_dev; use crate::devices::interfaces::has_dev;
/// Classify one free-form input string into the most specific query variant.
///
/// The current precedence is:
/// IP address, MAC address, neighbor state, interface name, then plain text.
pub async fn classify_query(q: String) -> QueryInput { pub async fn classify_query(q: String) -> QueryInput {
let s = parse::extract_host(&q); let s = parse::extract_host(&q);
if let Some(ip) = parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok()) { if let Some(ip) = parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok()) {
+6
View File
@@ -5,6 +5,7 @@ use wakey_core::{DhcpLease, DhcpLeaseWithState};
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json"; const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// Load the MAC-to-name cache used to preserve useful names across lease churn.
pub async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> { pub async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> {
match tokio::fs::read_to_string(MAC_NAME_CACHE).await { match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other), Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
@@ -13,12 +14,14 @@ pub async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<Stri
} }
} }
/// Persist the MAC-to-name cache back to disk.
async fn save_mac_name_cache(map: &std::collections::BTreeMap<String, String>) -> io::Result<()> { async fn save_mac_name_cache(map: &std::collections::BTreeMap<String, String>) -> io::Result<()> {
let s = serde_json::to_string(map).map_err(io::Error::other)?; let s = serde_json::to_string(map).map_err(io::Error::other)?;
let _ = tokio::fs::write(MAC_NAME_CACHE, s).await; let _ = tokio::fs::write(MAC_NAME_CACHE, s).await;
Ok(()) Ok(())
} }
/// Parse one `dnsmasq`-style DHCP lease line.
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLease> { pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLease> {
let mut c = line.split_whitespace(); let mut c = line.split_whitespace();
let expires_epoch: u64 = c.next()?.parse().ok()?; let expires_epoch: u64 = c.next()?.parse().ok()?;
@@ -33,6 +36,7 @@ pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLease> {
}) })
} }
/// Read raw DHCP leases from `/tmp/dhcp.leases`.
pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLease>> { pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLease>> {
match tokio::fs::read_to_string("/tmp/dhcp.leases").await { match tokio::fs::read_to_string("/tmp/dhcp.leases").await {
Ok(file) => Ok(file.lines().filter_map(parse_dhcp_lease_line).collect()), Ok(file) => Ok(file.lines().filter_map(parse_dhcp_lease_line).collect()),
@@ -41,6 +45,7 @@ pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLease>> {
} }
} }
/// Read DHCP leases and fill missing names from the MAC-name cache.
pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> { pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
let leases = read_dhcp_leases().await?; let leases = read_dhcp_leases().await?;
let mut cache = load_mac_name_cache().await.unwrap_or_default(); let mut cache = load_mac_name_cache().await.unwrap_or_default();
@@ -64,6 +69,7 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
Ok(leases_with_names) Ok(leases_with_names)
} }
/// Enrich DHCP leases with the best currently known neighbor state per IP.
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> { pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect(); let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, wakey_core::NeighborState> = let mut map: std::collections::HashMap<IpAddr, wakey_core::NeighborState> =
+6
View File
@@ -5,6 +5,7 @@ use macaddr::MacAddr;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use wakey_core::{WakeStatus, WakeTarget, WakeTargetResult}; use wakey_core::{WakeStatus, WakeTarget, WakeTargetResult};
/// Wake target with the minimum fields needed to send a magic packet.
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct CompleteWakeTarget { pub struct CompleteWakeTarget {
pub ip: IpAddr, pub ip: IpAddr,
@@ -27,6 +28,7 @@ impl TryFrom<WakeTarget> for CompleteWakeTarget {
} }
} }
/// Send one Wake-on-LAN magic packet to a complete target.
pub async fn wake_one(sock: &UdpSocket, t: CompleteWakeTarget) -> WakeTargetResult { pub async fn wake_one(sock: &UdpSocket, t: CompleteWakeTarget) -> WakeTargetResult {
let mac = t.mac; let mac = t.mac;
let mb = mac.as_bytes(); let mb = mac.as_bytes();
@@ -60,6 +62,10 @@ pub async fn wake_one(sock: &UdpSocket, t: CompleteWakeTarget) -> WakeTargetResu
} }
} }
/// Send Wake-on-LAN packets for many targets using one UDP socket.
///
/// Incomplete targets are not rejected with an error; they are returned as
/// `WakeStatus::Incomplete` result rows.
pub async fn wake_many( pub async fn wake_many(
targets: impl IntoIterator<Item = WakeTarget>, targets: impl IntoIterator<Item = WakeTarget>,
) -> io::Result<Vec<WakeTargetResult>> { ) -> io::Result<Vec<WakeTargetResult>> {