use std::collections::HashMap;
use std::io;
use std::net::{Ipv4Addr, UdpSocket};
use std::time::{Duration, Instant};
use a7n_core::focus::Take;
use a7n_proto::{MachineId, v1::Landing};
use super::{Daemon, DaemonError};
pub(super) const WAKE_RESEND_INTERVAL: Duration = Duration::from_secs(5);
pub(super) const WAKE_TIMEOUT: Duration = Duration::from_secs(60);
const WAKE_PORT: u16 = 9;
type PacketSender = Box<dyn FnMut([u8; 6]) -> io::Result<()> + Send>;
pub(super) struct Pending {
pub(super) peer: MachineId,
pub(super) deadline: Instant,
action: WakeAction,
}
pub(super) enum WakeAction {
Only,
Switch,
Edge(Landing),
}
pub(super) struct State {
pub(super) pending: Option<Pending>,
sent: HashMap<MachineId, Instant>,
pub(super) sender: PacketSender,
}
impl Default for State {
fn default() -> Self {
Self {
pending: None,
sent: HashMap::new(),
sender: Box::new(send_packet),
}
}
}
fn send_packet(mac: [u8; 6]) -> io::Result<()> {
let packet = a7n_core::wol::magic_packet(mac);
let mut targets = a7n_platform::ipv4_broadcasts();
targets.push((Ipv4Addr::UNSPECIFIED, Ipv4Addr::BROADCAST));
targets.sort_unstable();
targets.dedup();
let mut sent = false;
let mut error = None;
for (source, broadcast) in targets {
let result = (|| {
let socket = UdpSocket::bind((source, 0))?;
socket.set_broadcast(true)?;
socket.set_nonblocking(true)?;
socket.send_to(&packet, (broadcast, WAKE_PORT))
})();
match result {
Ok(_) => sent = true,
Err(failure) => error = Some(failure),
}
}
if sent {
Ok(())
} else {
Err(error.unwrap_or_else(|| io::Error::other("no broadcast interface")))
}
}
impl Daemon {
pub(super) fn is_linked(&self, peer: MachineId) -> bool {
self.links.by_peer.get(&peer).is_some_and(|handle| {
handle.connection.close_reason().is_none() && !handle.sender.is_closed()
})
}
pub(super) fn wake_peer(
&mut self,
peer: MachineId,
action: WakeAction,
now: Instant,
) -> Result<(), DaemonError> {
let entry = self.pins.get(&peer).ok_or(DaemonError::NotPaired)?;
if self.is_linked(peer) {
return Err(DaemonError::InvalidRequest("machine is already online"));
}
let mac = entry.hw_addr.ok_or(DaemonError::InvalidRequest(
"no hardware address known; connect once while awake first",
))?;
if self
.wol
.sent
.get(&peer)
.is_none_or(|last| now.saturating_duration_since(*last) >= WAKE_RESEND_INTERVAL)
{
(self.wol.sender)(mac)?;
self.wol.sent.retain(|id, _| self.pins.get(id).is_some());
self.wol.sent.insert(peer, now);
}
self.wol.pending = Some(Pending {
peer,
deadline: now + WAKE_TIMEOUT,
action,
});
Ok(())
}
pub(super) fn auto_wake(
&mut self,
peer: MachineId,
action: WakeAction,
now: Instant,
) -> Result<(), DaemonError> {
let same = self
.wol
.pending
.as_ref()
.is_some_and(|pending| pending.peer == peer && pending.deadline > now);
if !same {
self.wol.pending = None;
}
if self
.pins
.get(&peer)
.is_none_or(|entry| entry.hw_addr.is_none())
|| self.config.peer(&peer).is_some_and(|config| !config.wake)
{
return Err(DaemonError::NotConnected);
}
if same {
if let Some(pending) = &mut self.wol.pending
&& matches!(pending.action, WakeAction::Only)
{
pending.action = action;
}
return Ok(());
}
self.wake_peer(peer, action, now)
}
pub(super) async fn finish_wake(&mut self, now: Instant) {
let Some(pending) = &self.wol.pending else {
return;
};
if pending.deadline <= now || self.pins.get(&pending.peer).is_none() {
self.wol.pending = None;
return;
}
if !self.is_linked(pending.peer) {
return;
}
let pending = self.wol.pending.take().expect("pending wake checked above");
let landing = match pending.action {
WakeAction::Only => return,
WakeAction::Switch => {
let _ = self.switch_machine(pending.peer).await;
return;
}
WakeAction::Edge(landing) => landing,
};
if self.input.paused {
return;
}
let actions = self.input.focus.take(
Take {
target: pending.peer,
landing: Some(landing),
resistance_px: 0,
reach: self.links.by_peer.keys().copied().collect(),
},
now,
);
self.input_actions(actions).await;
}
}
#[cfg(test)]
mod tests;