wshub: persist calibration alongside sources; add config reload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
66efd74dd5
commit
dfd257cfd9
@@ -1,12 +1,12 @@
|
||||
package wshub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -95,11 +95,16 @@ func (sm *SourceManager) Remove(id string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Save writes the current source list to filePath.
|
||||
func (sm *SourceManager) Save() error {
|
||||
if sm.filePath == "" {
|
||||
return fmt.Errorf("no sources-file configured")
|
||||
}
|
||||
// Path returns the configured config-file path ("" when none).
|
||||
func (sm *SourceManager) Path() string {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
return sm.filePath
|
||||
}
|
||||
|
||||
// snapshotSources returns the current sources sorted by label, so the written
|
||||
// file is byte-stable across runs (the map iteration order is not).
|
||||
func (sm *SourceManager) snapshotSources() []SourceConfig {
|
||||
sm.mu.RLock()
|
||||
cfgs := make([]SourceConfig, 0, len(sm.sources))
|
||||
for _, ms := range sm.sources {
|
||||
@@ -111,26 +116,86 @@ func (sm *SourceManager) Save() error {
|
||||
})
|
||||
}
|
||||
sm.mu.RUnlock()
|
||||
sort.Slice(cfgs, func(i, j int) bool {
|
||||
if cfgs[i].Label != cfgs[j].Label {
|
||||
return cfgs[i].Label < cfgs[j].Label
|
||||
}
|
||||
return cfgs[i].Addr < cfgs[j].Addr
|
||||
})
|
||||
return cfgs
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cfgs, "", " ")
|
||||
// Save writes the current source list and calibration table to filePath as one
|
||||
// flat JSON array.
|
||||
func (sm *SourceManager) Save() error {
|
||||
path := sm.Path()
|
||||
if path == "" {
|
||||
return fmt.Errorf("no sources-file configured")
|
||||
}
|
||||
data, err := encodeConfigFile(sm.snapshotSources(), sm.hub.cal.List())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(sm.filePath, data, 0644)
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
// Load reads sources from path and adds them.
|
||||
// Load reads the config file at path, replaces the calibration table with its
|
||||
// contents and starts every source it lists.
|
||||
func (sm *SourceManager) Load(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cfgs []SourceConfig
|
||||
if err := json.Unmarshal(data, &cfgs); err != nil {
|
||||
srcs, cals, err := parseConfigFile(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sm.mu.Lock()
|
||||
sm.filePath = path
|
||||
for _, cfg := range cfgs {
|
||||
sm.mu.Unlock()
|
||||
|
||||
sm.hub.cal.Replace(cals)
|
||||
for _, cfg := range srcs {
|
||||
sm.Add(cfg.Label, cfg.Addr, cfg.MulticastGroup, cfg.DataPort)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload re-reads the config file. The calibration table is replaced wholesale
|
||||
// and sources listed in the file that are not already running are started; no
|
||||
// live source is ever stopped, restarted or reconnected, because a reload must
|
||||
// not interrupt streaming. The asymmetry is deliberate: calibration is cheap
|
||||
// to reapply, a source is a live UDP session.
|
||||
func (sm *SourceManager) Reload() error {
|
||||
path := sm.Path()
|
||||
if path == "" {
|
||||
return fmt.Errorf("no sources-file configured")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcs, cals, err := parseConfigFile(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sm.hub.cal.Replace(cals)
|
||||
|
||||
sm.mu.RLock()
|
||||
live := make(map[string]bool, len(sm.sources))
|
||||
for _, ms := range sm.sources {
|
||||
live[ms.label+"\x00"+ms.addr] = true
|
||||
}
|
||||
sm.mu.RUnlock()
|
||||
|
||||
for _, cfg := range srcs {
|
||||
label := cfg.Label
|
||||
if label == "" {
|
||||
label = cfg.Addr // Add() applies the same default
|
||||
}
|
||||
if live[label+"\x00"+cfg.Addr] {
|
||||
continue
|
||||
}
|
||||
sm.Add(cfg.Label, cfg.Addr, cfg.MulticastGroup, cfg.DataPort)
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user