fix gui
This commit is contained in:
@@ -3,7 +3,6 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -152,72 +151,6 @@ func TestLogBufferRedactsOnExport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanScannerConcurrentAccess(t *testing.T) {
|
||||
s := NewLanScanner(slog.New(slog.DiscardHandler))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
||||
defer cancel()
|
||||
s.Start(ctx)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Feed announcements the way the read loops do.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; ctx.Err() == nil; i++ {
|
||||
src := netip.AddrPortFrom(netip.MustParseAddr("192.168.1.50"), uint16(40000+i%3))
|
||||
s.handle(src, "[MOTD]§aTest §bServer[/MOTD][AD]25565[/AD]")
|
||||
s.handle(src, "malformed packet")
|
||||
}
|
||||
}()
|
||||
|
||||
// Read like the GUI does.
|
||||
for i := 0; i < 3; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for ctx.Err() == nil {
|
||||
for _, srv := range s.Servers() {
|
||||
_ = srv.Motd
|
||||
_ = srv.Addr
|
||||
}
|
||||
_ = s.Err()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Reconfigure while it runs.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for ctx.Err() == nil {
|
||||
s.SetSelfEntries([]LanEntry{{Motd: "Test Server", Port: 25565}})
|
||||
time.Sleep(time.Millisecond)
|
||||
s.SetSelfEntries(nil)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
servers := s.Servers()
|
||||
if len(servers) == 0 {
|
||||
t.Fatal("expected the synthetic announcements to be recorded")
|
||||
}
|
||||
for _, srv := range servers {
|
||||
if srv.Port != 25565 {
|
||||
t.Errorf("unexpected port %d", srv.Port)
|
||||
}
|
||||
// Colour codes must be stripped.
|
||||
if indexOf(srv.Motd, "§") >= 0 {
|
||||
t.Errorf("colour codes survived: %q", srv.Motd)
|
||||
}
|
||||
if srv.Motd != "Test Server" {
|
||||
t.Errorf("motd = %q, want %q", srv.Motd, "Test Server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerMonitorSnapshotIsIsolated(t *testing.T) {
|
||||
m := NewPeerMonitor(nil, nil, slog.New(slog.DiscardHandler), PeerMonitorOptions{})
|
||||
|
||||
|
||||
-539
@@ -1,539 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Minecraft's LAN discovery protocol: servers multicast the ASCII payload
|
||||
// "[MOTD]<motd>[/MOTD][AD]<port>[/AD]" to these groups roughly every 1.5s.
|
||||
// core/lan.go sends them; this file listens for them.
|
||||
const (
|
||||
lanScanGroupV4 = "224.0.2.60:4445"
|
||||
lanScanGroupV6 = "[ff75:230::60]:4445"
|
||||
)
|
||||
|
||||
const (
|
||||
// lanScanExpiry drops a server that stopped broadcasting.
|
||||
lanScanExpiry = 30 * time.Second
|
||||
// lanScanStale is how long a server may go unheard before the next packet
|
||||
// from it is treated as a real change worth waking the UI for. Without it
|
||||
// the GUI would redraw on every duplicate broadcast.
|
||||
lanScanStale = 10 * time.Second
|
||||
// lanScanSweep is the expiry tick interval.
|
||||
lanScanSweep = 5 * time.Second
|
||||
// lanScanBuf is the per-read buffer size; LAN announcements are tiny.
|
||||
lanScanBuf = 2048
|
||||
// lanScanMotdRunes caps a stored MOTD so a hostile peer cannot bloat the UI.
|
||||
lanScanMotdRunes = 120
|
||||
)
|
||||
|
||||
// LanServer is one Minecraft server seen broadcasting on the local network.
|
||||
type LanServer struct {
|
||||
Motd string // MOTD with Minecraft section-sign colour codes stripped
|
||||
RawMotd string // as received
|
||||
Port int //
|
||||
Source netip.AddrPort // who sent the packet
|
||||
Addr netip.Addr // Source.Addr(), the address to actually connect to
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
Count int // packets seen
|
||||
IsSelf bool // matches one of the entries tslink is advertising
|
||||
}
|
||||
|
||||
// lanScanKey deduplicates by sender address and advertised port. The sender's
|
||||
// ephemeral source port is deliberately excluded: it changes per socket.
|
||||
type lanScanKey struct {
|
||||
addr netip.Addr
|
||||
port int
|
||||
}
|
||||
|
||||
// LanScanner watches for Minecraft LAN broadcasts on every multicast-capable
|
||||
// interface and keeps a deduplicated, self-expiring view of what it heard.
|
||||
//
|
||||
// All methods are safe for concurrent use; the GUI calls [LanScanner.Servers]
|
||||
// from its frame loop while the read goroutines are writing.
|
||||
type LanScanner struct {
|
||||
logger *slog.Logger
|
||||
|
||||
mu sync.RWMutex
|
||||
servers map[lanScanKey]*LanServer
|
||||
self []LanEntry
|
||||
lastErr string
|
||||
subs map[int]chan struct{}
|
||||
nextSub int
|
||||
|
||||
started bool
|
||||
// live counts read loops still running. A VPN or virtual adapter going
|
||||
// down kills its socket's loop; when the last one dies the scanner is
|
||||
// deaf, and Err() has to say so instead of continuing to report health.
|
||||
live int
|
||||
}
|
||||
|
||||
// NewLanScanner returns a scanner that has not started listening yet. A nil
|
||||
// logger falls back to slog.Default.
|
||||
func NewLanScanner(logger *slog.Logger) *LanScanner {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &LanScanner{
|
||||
logger: logger.With(slog.String("from", "lanscan")),
|
||||
servers: make(map[lanScanKey]*LanServer),
|
||||
subs: make(map[int]chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetSelfEntries tells the scanner which advertisements are our own, so the UI
|
||||
// can distinguish "the tunnel is working" from "someone else is hosting". It
|
||||
// may be called after Start and re-evaluates already-known servers.
|
||||
func (s *LanScanner) SetSelfEntries(entries []LanEntry) {
|
||||
cp := make([]LanEntry, len(entries))
|
||||
copy(cp, entries)
|
||||
|
||||
s.mu.Lock()
|
||||
s.self = cp
|
||||
changed := false
|
||||
for _, srv := range s.servers {
|
||||
self := matchesSelf(cp, srv.RawMotd, srv.Port)
|
||||
if self != srv.IsSelf {
|
||||
srv.IsSelf = self
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
s.notifyLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Start begins listening; it returns immediately and stops when ctx is done.
|
||||
// Calling it twice is a no-op.
|
||||
func (s *LanScanner) Start(ctx context.Context) {
|
||||
s.mu.Lock()
|
||||
if s.started {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.started = true
|
||||
s.mu.Unlock()
|
||||
|
||||
conns := s.listen()
|
||||
if len(conns) == 0 {
|
||||
s.mu.Lock()
|
||||
s.lastErr = "no multicast listener could be created"
|
||||
// Clear the guard so a caller that notices Err() can retry once the
|
||||
// network stack is up. Binding can fail simply because Start ran
|
||||
// before the interfaces existed, and a permanently dead scanner is a
|
||||
// worse outcome than a redundant retry.
|
||||
s.started = false
|
||||
s.mu.Unlock()
|
||||
s.logger.Warn("lan scan disabled, all multicast binds failed")
|
||||
return
|
||||
}
|
||||
s.logger.With(slog.Int("sockets", len(conns))).Debug("lan scan listening")
|
||||
|
||||
// One closer goroutine unblocks every read at once on cancellation.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
s.mu.Lock()
|
||||
s.live = len(conns)
|
||||
s.mu.Unlock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, c := range conns {
|
||||
wg.Add(1)
|
||||
go func(c *net.UDPConn) {
|
||||
defer wg.Done()
|
||||
defer s.readerExited(ctx)
|
||||
s.readLoop(ctx, c)
|
||||
}(c)
|
||||
}
|
||||
go s.sweepLoop(ctx)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
s.logger.Debug("lan scan stopped")
|
||||
}()
|
||||
}
|
||||
|
||||
// listen joins the IPv4 group on every up, multicast-capable interface plus a
|
||||
// nil-interface fallback, then does the same for IPv6. Per-interface failures
|
||||
// are expected (containers, down VPN adapters) and only logged at debug level.
|
||||
func (s *LanScanner) listen() []*net.UDPConn {
|
||||
var conns []*net.UDPConn
|
||||
|
||||
v4, err := net.ResolveUDPAddr("udp4", lanScanGroupV4)
|
||||
if err != nil {
|
||||
s.logger.With(slog.String("error", err.Error())).Error("failed to resolve ipv4 multicast group")
|
||||
}
|
||||
v6, err := net.ResolveUDPAddr("udp6", lanScanGroupV6)
|
||||
if err != nil {
|
||||
s.logger.With(slog.String("error", err.Error())).Debug("failed to resolve ipv6 multicast group")
|
||||
}
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
s.logger.With(slog.String("error", err.Error())).Warn("failed to enumerate interfaces, falling back to default")
|
||||
ifaces = nil
|
||||
}
|
||||
|
||||
for i := range ifaces {
|
||||
ifi := ifaces[i]
|
||||
if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagMulticast == 0 {
|
||||
continue
|
||||
}
|
||||
if v4 != nil {
|
||||
if c, err := net.ListenMulticastUDP("udp4", &ifi, v4); err == nil {
|
||||
conns = append(conns, c)
|
||||
} else {
|
||||
s.logger.With(
|
||||
slog.String("iface", ifi.Name),
|
||||
slog.String("error", err.Error()),
|
||||
).Debug("ipv4 multicast join failed")
|
||||
}
|
||||
}
|
||||
if v6 != nil {
|
||||
if c, err := net.ListenMulticastUDP("udp6", &ifi, v6); err == nil {
|
||||
conns = append(conns, c)
|
||||
} else {
|
||||
s.logger.With(
|
||||
slog.String("iface", ifi.Name),
|
||||
slog.String("error", err.Error()),
|
||||
).Debug("ipv6 multicast join failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: let the OS pick the interface. On some hosts this is the only
|
||||
// socket that ever receives anything.
|
||||
if v4 != nil {
|
||||
if c, err := net.ListenMulticastUDP("udp4", nil, v4); err == nil {
|
||||
conns = append(conns, c)
|
||||
} else {
|
||||
s.logger.With(slog.String("error", err.Error())).Debug("default ipv4 multicast join failed")
|
||||
}
|
||||
}
|
||||
if v6 != nil {
|
||||
if c, err := net.ListenMulticastUDP("udp6", nil, v6); err == nil {
|
||||
conns = append(conns, c)
|
||||
} else {
|
||||
s.logger.With(slog.String("error", err.Error())).Debug("default ipv6 multicast join failed")
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range conns {
|
||||
_ = c.SetReadBuffer(64 * 1024)
|
||||
}
|
||||
return conns
|
||||
}
|
||||
|
||||
// readLoop drains one socket until ctx is done or the socket is closed. A
|
||||
// malformed packet is logged at debug level and never terminates the loop.
|
||||
func (s *LanScanner) readLoop(ctx context.Context, c *net.UDPConn) {
|
||||
buf := make([]byte, lanScanBuf)
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
// A deadline guarantees the loop notices cancellation even if the
|
||||
// closer goroutine has not run yet.
|
||||
_ = c.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
n, src, err := c.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
var nerr net.Error
|
||||
if errors.As(err, &nerr) && nerr.Timeout() {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
// Anything else (ENETDOWN from an adapter disappearing, for
|
||||
// instance) means this socket is finished. Release it here rather
|
||||
// than leaving the fd until the process exits; the ctx closer
|
||||
// goroutine would otherwise be the only thing that ever closes it.
|
||||
s.logger.With(slog.String("error", err.Error())).Debug("lan scan read failed")
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
if n <= 0 || src == nil {
|
||||
continue
|
||||
}
|
||||
ap, ok := netip.AddrFromSlice(src.IP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
s.handle(netip.AddrPortFrom(ap.Unmap(), uint16(src.Port)), string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
// readerExited records that one read loop finished. Once every socket is gone
|
||||
// while the scanner is still meant to be running, Err() must report it — the
|
||||
// UI otherwise shows a healthy "listening" chip over a scanner that will never
|
||||
// hear another packet.
|
||||
func (s *LanScanner) readerExited(ctx context.Context) {
|
||||
s.mu.Lock()
|
||||
if s.live > 0 {
|
||||
s.live--
|
||||
}
|
||||
dead := s.live == 0 && ctx.Err() == nil
|
||||
if dead {
|
||||
s.lastErr = "all multicast listeners stopped, restart to rescan"
|
||||
s.started = false
|
||||
s.notifyLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if dead {
|
||||
s.logger.Warn("lan scan has no live listeners left")
|
||||
}
|
||||
}
|
||||
|
||||
// sweepLoop expires servers that stopped broadcasting.
|
||||
func (s *LanScanner) sweepLoop(ctx context.Context) {
|
||||
t := time.NewTicker(lanScanSweep)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
s.expire(time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LanScanner) expire(now time.Time) {
|
||||
s.mu.Lock()
|
||||
changed := false
|
||||
for k, srv := range s.servers {
|
||||
if now.Sub(srv.LastSeen) > lanScanExpiry {
|
||||
delete(s.servers, k)
|
||||
changed = true
|
||||
s.logger.With(
|
||||
slog.String("addr", srv.Addr.String()),
|
||||
slog.Int("port", srv.Port),
|
||||
).Debug("lan server expired")
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
s.notifyLocked()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// handle records one parsed announcement.
|
||||
func (s *LanScanner) handle(src netip.AddrPort, payload string) {
|
||||
rawMotd, port, ok := parseLanAnnouncement(payload)
|
||||
if !ok {
|
||||
s.logger.With(
|
||||
slog.String("src", src.String()),
|
||||
slog.Int("len", len(payload)),
|
||||
).Debug("ignoring malformed lan announcement")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
key := lanScanKey{addr: src.Addr(), port: port}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
self := matchesSelf(s.self, rawMotd, port)
|
||||
if srv, ok := s.servers[key]; ok {
|
||||
// A repeat. Only wake the UI when something it renders actually moved.
|
||||
changed := srv.IsSelf != self || srv.RawMotd != rawMotd ||
|
||||
now.Sub(srv.LastSeen) > lanScanStale
|
||||
srv.LastSeen = now
|
||||
srv.Count++
|
||||
srv.RawMotd = rawMotd
|
||||
srv.Motd = cleanLanMotd(rawMotd)
|
||||
srv.IsSelf = self
|
||||
srv.Source = src
|
||||
if changed {
|
||||
s.notifyLocked()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.servers[key] = &LanServer{
|
||||
Motd: cleanLanMotd(rawMotd),
|
||||
RawMotd: rawMotd,
|
||||
Port: port,
|
||||
Source: src,
|
||||
Addr: src.Addr(),
|
||||
FirstSeen: now,
|
||||
LastSeen: now,
|
||||
Count: 1,
|
||||
IsSelf: self,
|
||||
}
|
||||
s.logger.With(
|
||||
slog.String("addr", src.Addr().String()),
|
||||
slog.Int("port", port),
|
||||
slog.Bool("self", self),
|
||||
).Debug("new lan server")
|
||||
s.notifyLocked()
|
||||
}
|
||||
|
||||
// Servers returns the currently-known servers, freshest first, safe to call
|
||||
// from the UI. The result is a copy: LanServer holds no reference types, so
|
||||
// the caller may read it without holding any lock.
|
||||
func (s *LanScanner) Servers() []LanServer {
|
||||
s.mu.RLock()
|
||||
out := make([]LanServer, 0, len(s.servers))
|
||||
for _, srv := range s.servers {
|
||||
out = append(out, *srv)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
// Deterministic ordering keeps the GUI from jittering between refreshes:
|
||||
// our own advertisements sink to the bottom, then freshest first.
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
a, b := out[i], out[j]
|
||||
if a.IsSelf != b.IsSelf {
|
||||
return !a.IsSelf
|
||||
}
|
||||
if !a.LastSeen.Equal(b.LastSeen) {
|
||||
return a.LastSeen.After(b.LastSeen)
|
||||
}
|
||||
if a.Port != b.Port {
|
||||
return a.Port < b.Port
|
||||
}
|
||||
return a.Source.String() < b.Source.String()
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// Err returns the last listener error, if the scanner could not bind at all.
|
||||
// It is empty while the scanner is healthy.
|
||||
func (s *LanScanner) Err() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.lastErr
|
||||
}
|
||||
|
||||
// Subscribe returns a channel that receives a value whenever the server set
|
||||
// meaningfully changes, plus a function that cancels the subscription. The
|
||||
// channel is buffered and coalescing: a slow reader sees one wakeup, not a
|
||||
// backlog of duplicate broadcasts.
|
||||
func (s *LanScanner) Subscribe() (<-chan struct{}, func()) {
|
||||
ch := make(chan struct{}, 1)
|
||||
s.mu.Lock()
|
||||
id := s.nextSub
|
||||
s.nextSub++
|
||||
s.subs[id] = ch
|
||||
s.mu.Unlock()
|
||||
|
||||
var once sync.Once
|
||||
cancel := func() {
|
||||
once.Do(func() {
|
||||
s.mu.Lock()
|
||||
delete(s.subs, id)
|
||||
s.mu.Unlock()
|
||||
})
|
||||
}
|
||||
return ch, cancel
|
||||
}
|
||||
|
||||
// notifyLocked wakes every subscriber. The caller must hold s.mu.
|
||||
func (s *LanScanner) notifyLocked() {
|
||||
for _, ch := range s.subs {
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default: // subscriber has a pending wakeup already
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// parseLanAnnouncement extracts the MOTD and port from a Minecraft LAN
|
||||
// broadcast. It is strict: anything not shaped exactly like
|
||||
// "[MOTD]…[/MOTD][AD]<1..65535>[/AD]" is rejected.
|
||||
func parseLanAnnouncement(payload string) (motd string, port int, ok bool) {
|
||||
motd, ok = between(payload, "[MOTD]", "[/MOTD]")
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
ad, ok := between(payload, "[AD]", "[/AD]")
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
port, err := strconv.Atoi(strings.TrimSpace(ad))
|
||||
if err != nil || !validPort(port) {
|
||||
return "", 0, false
|
||||
}
|
||||
return motd, port, true
|
||||
}
|
||||
|
||||
// between returns the text enclosed by the first open tag and the first close
|
||||
// tag that follows it.
|
||||
func between(s, openTag, closeTag string) (string, bool) {
|
||||
i := strings.Index(s, openTag)
|
||||
if i < 0 {
|
||||
return "", false
|
||||
}
|
||||
rest := s[i+len(openTag):]
|
||||
j := strings.Index(rest, closeTag)
|
||||
if j < 0 {
|
||||
return "", false
|
||||
}
|
||||
return rest[:j], true
|
||||
}
|
||||
|
||||
// cleanLanMotd strips Minecraft section-sign colour codes, trims whitespace and
|
||||
// caps the result so an oversized announcement cannot distort the UI.
|
||||
func cleanLanMotd(raw string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(raw))
|
||||
skip := false
|
||||
for _, r := range raw {
|
||||
if skip {
|
||||
// Drop the single formatting character following the section sign.
|
||||
skip = false
|
||||
continue
|
||||
}
|
||||
if r == '§' {
|
||||
skip = true
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
out := strings.TrimSpace(b.String())
|
||||
|
||||
n := 0
|
||||
for i := range out {
|
||||
n++
|
||||
if n > lanScanMotdRunes {
|
||||
return out[:i]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchesSelf reports whether an announcement corresponds to one of our own
|
||||
// advertised entries. Comparison uses the raw MOTD, which is exactly what
|
||||
// core/lan.go puts on the wire.
|
||||
func matchesSelf(self []LanEntry, rawMotd string, port int) bool {
|
||||
for _, e := range self {
|
||||
if e.Port == port && e.Motd == rawMotd {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -154,6 +154,7 @@ type PeerMonitor struct {
|
||||
|
||||
refreshStatus chan struct{}
|
||||
refreshPing chan struct{}
|
||||
refreshLinks chan struct{}
|
||||
|
||||
mu sync.RWMutex
|
||||
raw *ipnstate.Status // last good status, nil until the first poll lands
|
||||
@@ -180,6 +181,7 @@ func NewPeerMonitor(srv *tsnet.Server, rules map[string][]ConnectRule, logger *s
|
||||
opt: opt.withDefaults(),
|
||||
refreshStatus: make(chan struct{}, 1),
|
||||
refreshPing: make(chan struct{}, 1),
|
||||
refreshLinks: make(chan struct{}, 1),
|
||||
hist: make(map[string][]PeerSample),
|
||||
last: make(map[string]pingOutcome),
|
||||
links: make(map[netip.Addr][]string),
|
||||
@@ -196,9 +198,14 @@ func (m *PeerMonitor) Start(ctx context.Context) {
|
||||
}
|
||||
|
||||
// RefreshNow triggers an immediate status+ping cycle without blocking the caller.
|
||||
//
|
||||
// Link resolution is kicked too. It normally runs every linkResolveInterval,
|
||||
// but the GUI now lists only linked peers, so a user staring at an empty page
|
||||
// after a DNS hiccup has no other way to ask for a retry.
|
||||
func (m *PeerMonitor) RefreshNow() {
|
||||
kick(m.refreshStatus)
|
||||
kick(m.refreshPing)
|
||||
kick(m.refreshLinks)
|
||||
}
|
||||
|
||||
// kick delivers a coalescing wakeup: a pending signal is enough.
|
||||
@@ -534,6 +541,8 @@ func (m *PeerMonitor) linkLoop(ctx context.Context) {
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.resolveLinks(ctx)
|
||||
case <-m.refreshLinks:
|
||||
m.resolveLinks(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ type State struct {
|
||||
Config *Config
|
||||
Server *tsnet.Server
|
||||
Peers *PeerMonitor
|
||||
Lan *LanScanner
|
||||
}
|
||||
|
||||
// Ready reports whether the service finished booting.
|
||||
@@ -414,13 +413,8 @@ func (s *Supervisor) boot(ctx context.Context) error {
|
||||
peers := NewPeerMonitor(srv, cfg.Connect, s.logger, PeerMonitorOptions{})
|
||||
peers.Start(ctx)
|
||||
|
||||
lan := NewLanScanner(s.logger.With("from", "lan_scan"))
|
||||
lan.SetSelfEntries(LanEntriesFromRules(cfg.Connect))
|
||||
lan.Start(ctx)
|
||||
|
||||
s.update(func(st *State) {
|
||||
st.Peers = peers
|
||||
st.Lan = lan
|
||||
})
|
||||
s.stepDone(StepKeyMonitors, nil)
|
||||
|
||||
@@ -457,7 +451,6 @@ func (s *Supervisor) teardown() {
|
||||
srv := s.state.Server
|
||||
s.state.Server = nil
|
||||
s.state.Peers = nil
|
||||
s.state.Lan = nil
|
||||
s.mu.Unlock()
|
||||
|
||||
if srv != nil {
|
||||
|
||||
Reference in New Issue
Block a user