Listen() succeeding is not the same as being reachable. OpsLog binds 0.0.0.0, and Windows lets a second program bind the SAME port on the specific address 127.0.0.1. Connections to localhost then go to the more specific listener, so every client reaches the other program while ours sits there having logged "sharing CAT on port 4532" and never seeing a single connection. Found with Nexus, which starts its own rigctld on 127.0.0.1:4532 and talks to it. Neither side reports anything wrong: the operator gets a CAT timeout from a daemon with no radio behind it, and OpsLog's log is silent because nothing ever arrived. Three exchanges went into establishing that the connection simply never reached us — the port table was what settled it, not the code. So the server now dials its own port at startup and checks the connection lands on its own accept loop. If it does not, it says which program will be receiving the CAT connections and what to do. The counter it compares can only be raised by our own accept loop, so a real client arriving during the probe makes the check pass, never fail wrongly.
95 lines
2.4 KiB
Go
95 lines
2.4 KiB
Go
package rigctld
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A listener that binds successfully is not necessarily the one clients reach.
|
|
// Windows lets a second program bind the same port on the specific address
|
|
// 127.0.0.1 while ours holds 0.0.0.0, and localhost connections then go to the
|
|
// more specific one. Seen with Nexus, which starts its own rigctld on 4532.
|
|
func TestSelfTestWarnsWhenAnotherProgramHoldsLocalhost(t *testing.T) {
|
|
// Squat 127.0.0.1 first, the way the other program does.
|
|
squat, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Skipf("cannot bind localhost here: %v", err)
|
|
}
|
|
defer squat.Close()
|
|
port := squat.Addr().(*net.TCPAddr).Port
|
|
go func() {
|
|
for {
|
|
c, err := squat.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
c.Close()
|
|
}
|
|
}()
|
|
|
|
var mu sync.Mutex
|
|
var lines []string
|
|
s := New(port, nil, func(f string, a ...any) {
|
|
mu.Lock()
|
|
lines = append(lines, fmt.Sprintf(f, a...))
|
|
mu.Unlock()
|
|
})
|
|
if err := s.Start(); err != nil {
|
|
// The wildcard bind is refused on some setups; nothing to prove then.
|
|
t.Skipf("wildcard bind refused: %v", err)
|
|
}
|
|
defer s.Stop()
|
|
|
|
deadline := time.Now().Add(4 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
got := strings.Join(lines, "\n")
|
|
mu.Unlock()
|
|
if strings.Contains(got, "another program is already answering") {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
t.Errorf("no warning was logged while another listener owned localhost:%d.\nlog was:\n%s",
|
|
port, strings.Join(lines, "\n"))
|
|
}
|
|
|
|
// The healthy case must stay silent: a warning on every clean start would be
|
|
// noise, and noise in a log is what makes the real line easy to miss.
|
|
func TestSelfTestIsSilentWhenReachable(t *testing.T) {
|
|
// A real, free port: with 0 the OS picks one but s.port stays 0, so the probe
|
|
// would dial 127.0.0.1:0 and prove nothing.
|
|
probe, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Skipf("cannot bind: %v", err)
|
|
}
|
|
port := probe.Addr().(*net.TCPAddr).Port
|
|
probe.Close()
|
|
|
|
var mu sync.Mutex
|
|
var lines []string
|
|
s := New(port, nil, func(f string, a ...any) {
|
|
mu.Lock()
|
|
lines = append(lines, fmt.Sprintf(f, a...))
|
|
mu.Unlock()
|
|
})
|
|
if err := s.Start(); err != nil {
|
|
t.Skipf("start: %v", err)
|
|
}
|
|
defer s.Stop()
|
|
time.Sleep(1500 * time.Millisecond)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for _, l := range lines {
|
|
if strings.Contains(l, "WARNING") {
|
|
t.Errorf("a reachable port still warned: %q", l)
|
|
}
|
|
}
|
|
}
|