// 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: } } }