🎉 live server seems to be working now
This commit is contained in:
164
internal/live/mdns.go
Normal file
164
internal/live/mdns.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/mdns"
|
||||
)
|
||||
|
||||
// DiscoveredClient is a snapshot of one mDNS advertisement for a
|
||||
// Sendspin client (`_sendspin._tcp`). It does not include client_id —
|
||||
// that field is only known after WebSocket handshake. The HTTP layer
|
||||
// merges this snapshot with sendspin.Server.Clients() to produce the
|
||||
// full /api/clients view.
|
||||
type DiscoveredClient struct {
|
||||
Instance string `json:"instance"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
// Discovery continuously browses for _sendspin._tcp services. The list
|
||||
// of currently-known clients is exposed via List(). Entries persist for
|
||||
// `ttl` after their most recent sighting, then are pruned.
|
||||
type Discovery struct {
|
||||
ttl time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
clients map[string]DiscoveredClient
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewDiscovery() *Discovery {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Discovery{
|
||||
ttl: 2 * time.Minute,
|
||||
clients: make(map[string]DiscoveredClient),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discovery) Start() {
|
||||
go d.browseLoop()
|
||||
go d.pruneLoop()
|
||||
}
|
||||
|
||||
func (d *Discovery) Stop() {
|
||||
d.cancel()
|
||||
}
|
||||
|
||||
func (d *Discovery) List() []DiscoveredClient {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
out := make([]DiscoveredClient, 0, len(d.clients))
|
||||
for _, c := range d.clients {
|
||||
out = append(out, c)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Instance < out[j].Instance })
|
||||
return out
|
||||
}
|
||||
|
||||
// sendspinService matches entries advertised under `_sendspin._tcp` —
|
||||
// other devices on the LAN (WLED, Meshtastic, AirPlay, …) hit the same
|
||||
// multicast channel and hashicorp/mdns happily writes them to our
|
||||
// entries chan regardless of what we queried for. Filter by service
|
||||
// type before recording.
|
||||
const sendspinService = "_sendspin._tcp"
|
||||
|
||||
func (d *Discovery) record(entry *mdns.ServiceEntry) {
|
||||
if !strings.Contains(entry.Name, "."+sendspinService) {
|
||||
return
|
||||
}
|
||||
host := ""
|
||||
if entry.AddrV4 != nil {
|
||||
host = entry.AddrV4.String()
|
||||
} else if entry.AddrV6 != nil {
|
||||
host = entry.AddrV6.String()
|
||||
}
|
||||
if host == "" {
|
||||
return
|
||||
}
|
||||
instance := entry.Name
|
||||
if i := strings.Index(instance, "."); i > 0 {
|
||||
instance = instance[:i]
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.clients[instance] = DiscoveredClient{
|
||||
Instance: instance,
|
||||
Host: host,
|
||||
Port: entry.Port,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
var silentLogger = log.New(io.Discard, "", 0)
|
||||
|
||||
func (d *Discovery) browseLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
entries := make(chan *mdns.ServiceEntry, 32)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
for entry := range entries {
|
||||
d.record(entry)
|
||||
}
|
||||
}()
|
||||
|
||||
params := &mdns.QueryParam{
|
||||
Service: "_sendspin._tcp",
|
||||
Domain: "local",
|
||||
Timeout: 5 * time.Second,
|
||||
Entries: entries,
|
||||
Logger: silentLogger,
|
||||
}
|
||||
if err := mdns.Query(params); err != nil {
|
||||
log.Printf("mdns query: %v", err)
|
||||
}
|
||||
close(entries)
|
||||
<-done
|
||||
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case <-time.After(10 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discovery) pruneLoop() {
|
||||
t := time.NewTicker(30 * time.Second)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
cutoff := time.Now().Add(-d.ttl)
|
||||
d.mu.Lock()
|
||||
for k, v := range d.clients {
|
||||
if v.LastSeen.Before(cutoff) {
|
||||
delete(d.clients, k)
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user