Przeglądaj źródła

Fix control config persistence for restart-only changes

main
Jan 1 miesiąc temu
rodzic
commit
0aeb36054e
3 zmienionych plików z 132 dodań i 5 usunięć
  1. +14
    -1
      internal/control/control.go
  2. +111
    -0
      internal/control/control_test.go
  3. +7
    -4
      internal/control/server_test.go

+ 14
- 1
internal/control/control.go Wyświetl plik

@@ -714,7 +714,20 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
return
}
}
// Commit the server snapshot only after validation and any required live update succeeded.
// Persist the validated config snapshot when a config saver is available.
// This ensures restart-required UI changes survive process restarts instead
// of only updating the in-memory snapshot.
s.mu.RLock()
save := s.saveConfig
s.mu.RUnlock()
if save != nil {
if err := save(next); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Commit the server snapshot only after validation, optional persistence,
// and any required live update succeeded.
s.mu.Lock()
s.cfg = next
s.mu.Unlock()


+ 111
- 0
internal/control/control_test.go Wyświetl plik

@@ -740,6 +740,117 @@ func TestConfigPatchUpdatesSnapshot(t *testing.T) {
}
}

func TestConfigPatchPersistsRestartRequiredFieldsWhenSaverConfigured(t *testing.T) {
cfg := cfgpkg.Default()
srv := NewServer(cfg)

dir := t.TempDir()
configPath := filepath.Join(dir, "saved.json")
if err := cfgpkg.Save(configPath, cfg); err != nil {
t.Fatalf("seed config: %v", err)
}
srv.SetConfigSaver(func(next cfgpkg.Config) error {
return cfgpkg.Save(configPath, next)
})

rec := httptest.NewRecorder()
body := []byte(`{
"preEmphasisTauUS":75,
"audioGain":1.5,
"pi":"BEEF",
"pty":10,
"ms":false,
"ctEnabled":false,
"rtPlusEnabled":false,
"rtPlusSeparator":"/",
"ptyn":"ALTROCK",
"lps":"My Radio Station",
"ertEnabled":true,
"ert":"Grüezi mitenand",
"rds2Enabled":true,
"stationLogoPath":"C:\\logo.png",
"af":[93.3,95.7],
"bs412Enabled":true,
"bs412ThresholdDBr":0.5,
"mpxGain":1.3,
"compositeClipperIterations":4,
"compositeClipperSoftKnee":0.22,
"compositeClipperLookaheadMs":1.4
}`)
srv.Handler().ServeHTTP(rec, newConfigPostRequest(body))
if rec.Code != http.StatusOK {
t.Fatalf("status: %d body=%s", rec.Code, rec.Body.String())
}

saved, err := cfgpkg.Load(configPath)
if err != nil {
t.Fatalf("load saved config: %v", err)
}
if saved.FM.PreEmphasisTauUS != 75 {
t.Fatalf("expected saved preEmphasisTauUS=75, got %v", saved.FM.PreEmphasisTauUS)
}
if saved.Audio.Gain != 1.5 {
t.Fatalf("expected saved audio.gain=1.5, got %v", saved.Audio.Gain)
}
if saved.RDS.PI != "BEEF" {
t.Fatalf("expected saved rds.pi=BEEF, got %q", saved.RDS.PI)
}
if saved.RDS.PTY != 10 {
t.Fatalf("expected saved rds.pty=10, got %v", saved.RDS.PTY)
}
if saved.RDS.MS != false {
t.Fatalf("expected saved rds.ms=false, got %v", saved.RDS.MS)
}
if saved.RDS.CTEnabled != false {
t.Fatalf("expected saved rds.ctEnabled=false, got %v", saved.RDS.CTEnabled)
}
if saved.RDS.RTPlusEnabled != false {
t.Fatalf("expected saved rds.rtPlusEnabled=false, got %v", saved.RDS.RTPlusEnabled)
}
if saved.RDS.RTPlusSeparator != "/" {
t.Fatalf("expected saved rds.rtPlusSeparator='/', got %q", saved.RDS.RTPlusSeparator)
}
if saved.RDS.PTYN != "ALTROCK" {
t.Fatalf("expected saved rds.ptyn=ALTROCK, got %q", saved.RDS.PTYN)
}
if saved.RDS.LPS != "My Radio Station" {
t.Fatalf("expected saved rds.lps, got %q", saved.RDS.LPS)
}
if saved.RDS.ERTEnabled != true {
t.Fatalf("expected saved rds.ertEnabled=true, got %v", saved.RDS.ERTEnabled)
}
if saved.RDS.ERT != "Grüezi mitenand" {
t.Fatalf("expected saved rds.ert, got %q", saved.RDS.ERT)
}
if saved.RDS.RDS2Enabled != true {
t.Fatalf("expected saved rds.rds2Enabled=true, got %v", saved.RDS.RDS2Enabled)
}
if saved.RDS.StationLogoPath != "C:\\logo.png" {
t.Fatalf("expected saved rds.stationLogoPath, got %q", saved.RDS.StationLogoPath)
}
if len(saved.RDS.AF) != 2 || saved.RDS.AF[0] != 93.3 || saved.RDS.AF[1] != 95.7 {
t.Fatalf("expected saved rds.af=[93.3 95.7], got %v", saved.RDS.AF)
}
if !saved.FM.BS412Enabled {
t.Fatalf("expected saved bs412Enabled=true, got false")
}
if saved.FM.BS412ThresholdDBr != 0.5 {
t.Fatalf("expected saved bs412ThresholdDBr=0.5, got %v", saved.FM.BS412ThresholdDBr)
}
if saved.FM.MpxGain != 1.3 {
t.Fatalf("expected saved fm.mpxGain=1.3, got %v", saved.FM.MpxGain)
}
if saved.FM.CompositeClipper.Iterations != 4 {
t.Fatalf("expected saved compositeClipper.iterations=4, got %v", saved.FM.CompositeClipper.Iterations)
}
if saved.FM.CompositeClipper.SoftKnee != 0.22 {
t.Fatalf("expected saved compositeClipper.softKnee=0.22, got %v", saved.FM.CompositeClipper.SoftKnee)
}
if saved.FM.CompositeClipper.LookaheadMs != 1.4 {
t.Fatalf("expected saved compositeClipper.lookaheadMs=1.4, got %v", saved.FM.CompositeClipper.LookaheadMs)
}
}

func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) {
srv := NewServer(cfgpkg.Default())
srv.SetTXController(&fakeTXController{updateErr: errors.New("boom")})


+ 7
- 4
internal/control/server_test.go Wyświetl plik

@@ -18,11 +18,14 @@ func TestNewHTTPServerConfig(t *testing.T) {
if srv.Handler != handler {
t.Fatalf("expected handler to be preserved")
}
if srv.ReadTimeout != defaultReadTimeout {
t.Fatalf("expected read timeout %s, got %s", defaultReadTimeout, srv.ReadTimeout)
if srv.ReadTimeout != 0 {
t.Fatalf("expected read timeout to remain disabled for streaming requests, got %s", srv.ReadTimeout)
}
if srv.WriteTimeout != defaultWriteTimeout {
t.Fatalf("expected write timeout %s, got %s", defaultWriteTimeout, srv.WriteTimeout)
if srv.WriteTimeout != 0 {
t.Fatalf("expected write timeout to remain disabled for streaming requests, got %s", srv.WriteTimeout)
}
if srv.ReadHeaderTimeout != defaultReadHeaderTimeout {
t.Fatalf("expected read header timeout %s, got %s", defaultReadHeaderTimeout, srv.ReadHeaderTimeout)
}
if srv.IdleTimeout != defaultIdleTimeout {
t.Fatalf("expected idle timeout %s, got %s", defaultIdleTimeout, srv.IdleTimeout)


Ładowanie…
Anuluj
Zapisz