Wake: a target may list several broadcast addresses

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)
This commit is contained in:
2026-09-25 13:45:39 -07:00
parent 4e1dd03d07
commit 8018f67031
6 changed files with 146 additions and 14 deletions
+41 -8
View File
@@ -7,15 +7,25 @@ package wake
import (
"context"
"fmt"
"log/slog"
"net"
"sync"
"time"
)
// Target describes how to wake one named host.
// 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, Broadcast string // MAC "aa:bb:cc:dd:ee:ff" (any separator, any case); Broadcast "host:port"
Wait time.Duration
MAC string
Broadcast string
Broadcasts []string
Wait time.Duration
}
// Health reports whether a named host is currently healthy. Implementations must
@@ -67,6 +77,26 @@ func Send(mac, broadcast string) error {
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 {
@@ -95,10 +125,13 @@ func (w *Waker) PollEvery(d time.Duration) {
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.
// 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]
@@ -110,7 +143,7 @@ func (w *Waker) Wake(ctx context.Context, host string) bool {
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 {
if !sendAll(target.MAC, target.Broadcast, target.Broadcasts) {
return false
}
w.mu.Lock()