Config keeps broadcast (one) and adds broadcasts (a list); exactly one must be present. Wake.Addresses() returns Broadcast then Broadcasts; checkWake errors on both-set, neither-or-empty, and non-host:port entries. Waker sends to every address in order, logging/past a failure so one dead address does not stop the others, and returns false only when none could be sent. Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
// Package wake sends wake-on-LAN magic packets and waits for a sleeping host to
|
|
// appear healthy in the health table. A sleeping host takes tens of seconds to
|
|
// come up, so the Waker remembers when it last sent and wakes a host at most once
|
|
// per wait window.
|
|
package wake
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// log reports a broadcast that fails to resolve or send. Wake continues past
|
|
// such failures (rule: one dead address must not stop the others), so this is
|
|
// the only place the package logs; the address and error are not request data.
|
|
var log = slog.New(slog.Default().Handler())
|
|
|
|
// Target describes how to wake one named host. Broadcast is the single-address
|
|
// form (as before); Broadcasts names more than one (a host that roams between
|
|
// networks). Wake sends to Broadcast (if set) and then each of Broadcasts.
|
|
type Target struct {
|
|
MAC string
|
|
Broadcast string
|
|
Broadcasts []string
|
|
Wait time.Duration
|
|
}
|
|
|
|
// Health reports whether a named host is currently healthy. Implementations must
|
|
// be safe for concurrent use.
|
|
type Health interface{ Healthy(name string) bool }
|
|
|
|
// magicPacketLen is six sync bytes plus the MAC repeated sixteen times.
|
|
const magicPacketLen = 6 + 6*16
|
|
|
|
// MagicPacket builds a wake-on-LAN magic packet: six 0xff bytes followed by the
|
|
// target MAC sixteen times, a 102-byte frame.
|
|
func MagicPacket(mac string) ([]byte, error) {
|
|
m, err := net.ParseMAC(mac)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wake: parse MAC %q: %w", mac, err)
|
|
}
|
|
if len(m) != 6 {
|
|
return nil, fmt.Errorf("wake: MAC %q is not six bytes", mac)
|
|
}
|
|
pkt := make([]byte, magicPacketLen)
|
|
for i := range pkt[:6] {
|
|
pkt[i] = 0xff
|
|
}
|
|
for i := 0; i < 16; i++ {
|
|
copy(pkt[6+i*6:], m)
|
|
}
|
|
return pkt, nil
|
|
}
|
|
|
|
// Send emits one magic packet for mac to the broadcast address as a single UDP4
|
|
// datagram, reporting parse, resolve and write errors.
|
|
func Send(mac, broadcast string) error {
|
|
pkt, err := MagicPacket(mac)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
remote, err := net.ResolveUDPAddr("udp4", broadcast)
|
|
if err != nil {
|
|
return fmt.Errorf("wake: resolve broadcast %q: %w", broadcast, err)
|
|
}
|
|
conn, err := net.DialUDP("udp4", nil, remote)
|
|
if err != nil {
|
|
return fmt.Errorf("wake: dial broadcast %q: %w", broadcast, err)
|
|
}
|
|
defer conn.Close()
|
|
if _, err := conn.Write(pkt); err != nil {
|
|
return fmt.Errorf("wake: write packet to %q: %w", broadcast, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sendAll emits one magic packet for mac to broadcast (if non-empty) and then
|
|
// to each address in the rest, in order. An address that fails to resolve or
|
|
// send is logged and does not stop the others; it reports whether at least one
|
|
// packet went out.
|
|
func sendAll(mac, broadcast string, rest []string) bool {
|
|
addrs := append([]string{broadcast}, rest...)
|
|
sent := false
|
|
for _, addr := range addrs {
|
|
if addr == "" {
|
|
continue
|
|
}
|
|
if err := Send(mac, addr); err != nil {
|
|
log.Error("wake broadcast failed", "addr", addr, "err", err)
|
|
continue
|
|
}
|
|
sent = true
|
|
}
|
|
return sent
|
|
}
|
|
|
|
// Waker wakes named hosts at most once per wait window and waits for the health
|
|
// table to report them healthy. It is safe for concurrent Wake calls.
|
|
type Waker struct {
|
|
mu sync.Mutex
|
|
targets map[string]Target
|
|
health Health
|
|
lastSent map[string]time.Time
|
|
poll time.Duration
|
|
}
|
|
|
|
// New returns a Waker for the given targets, polling health every second.
|
|
func New(targets map[string]Target, h Health) *Waker {
|
|
return &Waker{
|
|
targets: targets,
|
|
health: h,
|
|
lastSent: make(map[string]time.Time),
|
|
poll: time.Second,
|
|
}
|
|
}
|
|
|
|
// PollEvery sets how often Wake re-checks health; it is a test hook. Production
|
|
// keeps the 1 s default from New.
|
|
func (w *Waker) PollEvery(d time.Duration) {
|
|
w.mu.Lock()
|
|
w.poll = d
|
|
w.mu.Unlock()
|
|
}
|
|
|
|
// Wake sends a magic packet for host to every broadcast address — Broadcast
|
|
// (if set) then each of Broadcasts, in order — if none was sent in the last
|
|
// Wait, then polls health until the host is healthy, the wait elapses, or ctx is
|
|
// done. A broadcast that fails to resolve or send is logged and does not stop
|
|
// the others; Wake returns true only when the host becomes healthy, and false
|
|
// for an unknown host, on timeout, when ctx ends first, or when no address
|
|
// could be sent to.
|
|
func (w *Waker) Wake(ctx context.Context, host string) bool {
|
|
w.mu.Lock()
|
|
target, ok := w.targets[host]
|
|
if !ok {
|
|
w.mu.Unlock()
|
|
return false
|
|
}
|
|
now := time.Now()
|
|
if last, sent := w.lastSent[host]; !sent || now.Sub(last) >= target.Wait {
|
|
w.lastSent[host] = now
|
|
w.mu.Unlock()
|
|
if !sendAll(target.MAC, target.Broadcast, target.Broadcasts) {
|
|
return false
|
|
}
|
|
w.mu.Lock()
|
|
}
|
|
poll := w.poll
|
|
deadline := now.Add(target.Wait)
|
|
w.mu.Unlock()
|
|
|
|
timer := time.NewTimer(poll)
|
|
defer timer.Stop()
|
|
for {
|
|
if w.health.Healthy(host) {
|
|
return true
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return false
|
|
}
|
|
d := poll
|
|
if rem := time.Until(deadline); rem < d {
|
|
d = rem
|
|
}
|
|
timer.Reset(d)
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
}
|