mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
feat(health): add server_url self-reachability check + log actual listen port
The most common misconfiguration on install-on-speaker setups is an HTTP server URL that omits the port (e.g. http://192.0.2.1 instead of http://192.0.2.1:8000). Port 80 is occupied by the Bose firmware's PtsServer, so AfterTouch binds its default port 8000 — but the margeURL pushed to speakers still resolves to port 80 and hits PtsServer instead of AfterTouch. Marge calls are silently dropped, sources are never registered, and TuneIn playback fails with error 1005 (UNKNOWN_SOURCE_ERROR). See issue #319. Changes: - pkg/service/health/checks_server_url.go — new health check (server_url_reachable) that probes GET {serverURL}/setup/version from inside the service; emits SeverityWarning with remediation steps when the endpoint is not reachable or returns non-200. - pkg/service/handlers/server.go — register the new check in NewServer. - cmd/soundtouch-service/main.go — replace http.ListenAndServe with an explicit net.Listen so the true effective port is logged before TLS starts. Both HTTP and HTTPS log lines now show the listener's actual bound address alongside the configured server URL: Go service listening on 0.0.0.0:8000 (server URL: http://192.0.2.1) Previously only the server URL was logged, creating the false impression that AfterTouch had bound that URL's implicit port. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
93bc04b334
commit
417c0223dd
@@ -518,7 +518,16 @@ func main() {
|
||||
|
||||
r := setupRouter(server, stockholmHandler)
|
||||
|
||||
log.Printf("Go service starting on %s", sanitizeLog(config.serverURL))
|
||||
// Bind the listener before logging so we print the true
|
||||
// effective port (handles :0 and catches "address already
|
||||
// in use" before the TLS goroutine launches).
|
||||
ln, err := net.Listen("tcp", config.addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", config.addr, err)
|
||||
}
|
||||
|
||||
log.Printf("Go service listening on %s (server URL: %s)",
|
||||
ln.Addr().String(), sanitizeLog(config.serverURL))
|
||||
|
||||
// TLS cert generation can be slow on constrained hardware; run it in the
|
||||
// background so the HTTP server is available immediately.
|
||||
@@ -536,7 +545,7 @@ func main() {
|
||||
runHTTPSPreflight(config.httpsServerURL, config.serverURL, config.dnsEnabled, server.ResolveServerURLIPForPreflight)
|
||||
}()
|
||||
|
||||
return http.ListenAndServe(config.addr, r)
|
||||
return http.Serve(ln, r)
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
@@ -1306,8 +1315,6 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
|
||||
ErrorLog: log.Default(), // Ensure error logging is enabled
|
||||
}
|
||||
|
||||
log.Printf("Go service starting HTTPS on %s", sanitizeLog(httpsServerURL))
|
||||
|
||||
go func() {
|
||||
listener, err := net.Listen("tcp", httpsAddr)
|
||||
if err != nil {
|
||||
@@ -1315,6 +1322,9 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Go service listening HTTPS on %s (server URL: %s)",
|
||||
listener.Addr().String(), sanitizeLog(httpsServerURL))
|
||||
|
||||
tlsListener := tls.NewListener(listener, tlsConfig)
|
||||
|
||||
// Wrap listener to log connection attempts
|
||||
|
||||
@@ -135,6 +135,10 @@ func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, red
|
||||
health.RegisterRefreshSourcesCheck(s.healthRegistry, ds)
|
||||
health.RegisterStaleInternetRadioCheck(s.healthRegistry, ds)
|
||||
health.RegisterDefaultAccountNonBoseDevicesCheck(s.healthRegistry, ds)
|
||||
health.RegisterServerURLReachableCheck(s.healthRegistry, func() string {
|
||||
serverURL, _ := s.GetSettings()
|
||||
return serverURL
|
||||
})
|
||||
health.RegisterOAuthTargetReachableCheck(
|
||||
s.healthRegistry,
|
||||
func() string {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckIDServerURLReachable is the registry id of the server-URL
|
||||
// self-reachability check.
|
||||
const CheckIDServerURLReachable = "server_url_reachable"
|
||||
|
||||
// RegisterServerURLReachableCheck registers the server_url_reachable
|
||||
// check. It probes GET {serverURL}/setup/version from inside the
|
||||
// service. If the request fails or returns a non-200 status, the
|
||||
// configured server URL doesn't route back to AfterTouch — speakers
|
||||
// pointing their margeURL at that address will receive errors instead
|
||||
// of marge responses.
|
||||
//
|
||||
// The most common cause is a missing port number in the server URL:
|
||||
// e.g. "http://192.0.2.1" implies port 80, but if another process
|
||||
// (such as the Bose firmware's PtsServer) already occupies port 80,
|
||||
// AfterTouch runs on its default port (8000) instead — and all marge
|
||||
// calls from speakers silently hit the wrong process.
|
||||
//
|
||||
// getServerURL is a closure so the check picks up config changes
|
||||
// without re-registration.
|
||||
func RegisterServerURLReachableCheck(r *Registry, getServerURL func() string) {
|
||||
r.Register(Check{
|
||||
ID: CheckIDServerURLReachable,
|
||||
Title: "Service is reachable at configured server URL",
|
||||
Run: func() []Finding {
|
||||
return runServerURLReachableCheck(getServerURL())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func runServerURLReachableCheck(serverURL string) []Finding {
|
||||
if strings.TrimSpace(serverURL) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
probeURL := strings.TrimRight(serverURL, "/") + "/setup/version"
|
||||
res := ProbeGet(context.Background(), probeURL, 2*time.Second)
|
||||
|
||||
if res.Reachable && res.Status == 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errDetail := res.Err
|
||||
if errDetail == "" && res.Status != 0 {
|
||||
errDetail = fmt.Sprintf("HTTP %d", res.Status)
|
||||
}
|
||||
|
||||
details := fmt.Sprintf(
|
||||
"Probe: GET %s → %s. "+
|
||||
"The most common cause is a missing port in the server URL. "+
|
||||
"Example: \"http://192.0.2.1\" implies port 80; if another process "+
|
||||
"(e.g. the Bose firmware's PtsServer) occupies port 80, AfterTouch runs on its "+
|
||||
"default port 8000 instead. Fix: add the explicit port, e.g. \"http://192.0.2.1:8000\", "+
|
||||
"then restart the service and re-apply the migration for each speaker to push the "+
|
||||
"updated margeURL.",
|
||||
probeURL, errDetail,
|
||||
)
|
||||
|
||||
return []Finding{{
|
||||
Severity: SeverityWarning,
|
||||
Message: fmt.Sprintf(
|
||||
"Configured server URL %q is not reachable from inside the service (probe: %s). "+
|
||||
"Marge calls from speakers will fail — check that the URL and port match "+
|
||||
"the service's actual listening port.",
|
||||
serverURL, errDetail,
|
||||
),
|
||||
Details: details,
|
||||
ManualCommands: []ManualCommand{
|
||||
{
|
||||
Label: "Check what port AfterTouch is listening on:",
|
||||
Command: "ss -tlnp | grep soundtouch",
|
||||
Hint: "Compare the listening port with the port implied by the configured server URL.",
|
||||
},
|
||||
{
|
||||
Label: "Or set the correct URL via the web UI:",
|
||||
Command: "Settings tab → Server URL (Target Domain) → add explicit port (e.g. :8000) → Save → restart",
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServerURLReachableCheck_PassesWhenVersionEndpointReturns200(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/setup/version" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"version":"test"}`))
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
got := runServerURLReachableCheck(srv.URL)
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected no findings when /setup/version returns 200, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerURLReachableCheck_WarnsWhenVersionEndpointReturns404(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
got := runServerURLReachableCheck(srv.URL)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected one finding when /setup/version returns 404, got %+v", got)
|
||||
}
|
||||
|
||||
if got[0].Severity != SeverityWarning {
|
||||
t.Errorf("expected SeverityWarning, got %v", got[0].Severity)
|
||||
}
|
||||
|
||||
if !strings.Contains(got[0].Message, srv.URL) {
|
||||
t.Errorf("expected server URL in message, got %q", got[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerURLReachableCheck_WarnsWhenServerUnreachable(t *testing.T) {
|
||||
// Use a port that is almost certainly not listening.
|
||||
got := runServerURLReachableCheck("http://127.0.0.1:19999")
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected one finding for unreachable server, got %+v", got)
|
||||
}
|
||||
|
||||
if got[0].Severity != SeverityWarning {
|
||||
t.Errorf("expected SeverityWarning, got %v", got[0].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerURLReachableCheck_EmptyURLIsNoOp(t *testing.T) {
|
||||
for _, url := range []string{"", " "} {
|
||||
got := runServerURLReachableCheck(url)
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected no findings for empty URL %q, got %+v", url, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user