Add the wake package: magic packets and a waiter
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// 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"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Target describes how to wake one named host.
|
||||
type Target struct {
|
||||
MAC, Broadcast string // MAC "aa:bb:cc:dd:ee:ff" (any separator, any case); Broadcast "host:port"
|
||||
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
|
||||
}
|
||||
|
||||
// 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 if none was sent in the last Wait, then
|
||||
// polls health until the host is healthy, the wait elapses, or ctx is done. It
|
||||
// returns true only when the host becomes healthy, and false for an unknown
|
||||
// host, on timeout, or when ctx ends first.
|
||||
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 err := Send(target.MAC, target.Broadcast); err != nil {
|
||||
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:
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user