From 02f60932ce872ececc98f349c1f9b50ad9facdae Mon Sep 17 00:00:00 2001 From: Stanislav Fifik Date: Sun, 14 Jun 2026 12:47:56 +0200 Subject: [PATCH] feat(probe): add diagnostic probe for mDNS advertisement and WebSocket communication --- cmd/probe/main.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 cmd/probe/main.go diff --git a/cmd/probe/main.go b/cmd/probe/main.go new file mode 100644 index 0000000..35e3fd8 --- /dev/null +++ b/cmd/probe/main.go @@ -0,0 +1,92 @@ +// Diagnostic probe: advertise _sendspin._tcp via mDNS, listen on /sendspin, +// and log exactly what the server (Music Assistant) sends after it dials us — +// WITHOUT sending anything first. Reveals whether MA expects to speak first +// (role flip) or waits for our client/hello. +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/Sendspin/sendspin-go/pkg/discovery" + "github.com/gorilla/websocket" +) + +func main() { + name := flag.String("name", hostname(), "advertised name") + port := flag.Int("port", 8928, "listen + mDNS port") + speakFirst := flag.Bool("speak-first", false, "send a server/hello before reading (test role-flip hypothesis)") + flag.Parse() + + disc := discovery.NewManager(discovery.Config{ServiceName: *name, Port: *port}) + if err := disc.Advertise(); err != nil { + log.Printf("mdns advertise: %v", err) + } else { + defer disc.Stop() + } + + upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} + mux := http.NewServeMux() + mux.HandleFunc("/sendspin", func(w http.ResponseWriter, r *http.Request) { + log.Printf("=== incoming dial from %s (headers: subproto=%q origin=%q UA=%q) ===", + r.RemoteAddr, r.Header.Get("Sec-WebSocket-Protocol"), r.Header.Get("Origin"), r.Header.Get("User-Agent")) + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("upgrade failed: %v", err) + return + } + defer conn.Close() + + if *speakFirst { + hello := `{"type":"server/hello","payload":{"server_id":"probe","name":"probe","version":1,"active_roles":["player@v1"],"connection_reason":"discovery"}}` + log.Printf("-> sending server/hello: %s", hello) + if err := conn.WriteMessage(websocket.TextMessage, []byte(hello)); err != nil { + log.Printf("write failed: %v", err) + } + } + + log.Printf("listening for first message from server (10s)...") + for { + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + mt, data, err := conn.ReadMessage() + if err != nil { + log.Printf("read ended: %v", err) + if ce, ok := err.(*websocket.CloseError); ok { + log.Printf("CLOSE code=%d text=%q", ce.Code, ce.Text) + } + return + } + if mt == websocket.BinaryMessage { + log.Printf("<- SERVER binary %d bytes", len(data)) + } else { + log.Printf("<- SERVER text: %s", string(data)) + } + } + }) + + srv := &http.Server{Addr: fmt.Sprintf(":%d", *port), Handler: mux} + go func() { + log.Printf("probe listening on :%d/sendspin as %q", *port, *name) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %v", err) + } + }() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + <-sig +} + +func hostname() string { + h, _ := os.Hostname() + if h == "" { + return "sendspin-probe" + } + return h + "-probe" +}