fix(ultrabeam): match replies to requests by seq (stop stream desync)
sendCommand discarded the reply's sequence number and accepted whatever frame came back. On a slow remote link a command that timed out left its late reply in the stream, and the NEXT command read it as its own — crossing STATUS with READ_BANDS/PROGRESS. That surfaced as phantom frequency jumps (a spurious follow-loop re-tune), intermittent "reply too short" disconnects, and wrong element-length readings (READ_BANDS getting the status frame). Now every reply is matched to its request seq and stale/malformed frames are drained (bounded), with readPacket resyncing to the next STX. Tested with a net.Pipe that injects a stale reply ahead of the real one.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package ultrabeam
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A stale reply left in the stream by an earlier timed-out command must be
|
||||
// DRAINED, not taken for this command's reply. Without seq matching, a STATUS
|
||||
// query would return a lingering READ_BANDS/PROGRESS payload — which showed up
|
||||
// on a remote link as phantom frequencies (a spurious follow-loop re-tune) and
|
||||
// dropped connections.
|
||||
func TestSendCommandDrainsStaleReplyAndMatchesSeq(t *testing.T) {
|
||||
srvConn, cliConn := net.Pipe()
|
||||
defer srvConn.Close()
|
||||
defer cliConn.Close()
|
||||
|
||||
c := &Client{conn: cliConn, reader: bufio.NewReader(cliConn)}
|
||||
srv := &Client{reader: bufio.NewReader(srvConn)}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
// Read the request, learn its seq, then answer with a stale reply (wrong
|
||||
// seq) FIRST, then the real one.
|
||||
req, err := srv.readPacket()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
seq, _, _, err := parsePacket(req)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
staleSeq := (seq + 1) % 128
|
||||
if _, err := srvConn.Write(c.buildPacket(staleSeq, UB_OK, []byte{0xAA, 0xBB})); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
if _, err := srvConn.Write(c.buildPacket(seq, UB_OK, []byte{0x11, 0x22, 0x33})); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
errc <- nil
|
||||
}()
|
||||
|
||||
payload, err := c.sendCommand(CMD_STATUS, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("sendCommand: %v", err)
|
||||
}
|
||||
if !bytes.Equal(payload, []byte{0x11, 0x22, 0x33}) {
|
||||
t.Fatalf("payload = % X, want 11 22 33 — stale reply not drained or seq not matched", payload)
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// readPacket must resynchronise to the next STX, dropping any partial/garbage
|
||||
// bytes left in the stream, so one corrupt frame can't misalign every frame
|
||||
// after it.
|
||||
func TestReadPacketResyncsToSTX(t *testing.T) {
|
||||
frame := (&Client{}).buildPacket(5, UB_OK, []byte{0x01, 0x02})
|
||||
stream := append([]byte{0x11, 0x22, 0x33}, frame...) // leading garbage, then a real frame
|
||||
c := &Client{reader: bufio.NewReader(bytes.NewReader(stream))}
|
||||
|
||||
got, err := c.readPacket()
|
||||
if err != nil {
|
||||
t.Fatalf("readPacket: %v", err)
|
||||
}
|
||||
seq, cmd, payload, err := parsePacket(got)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePacket: %v", err)
|
||||
}
|
||||
if seq != 5 || cmd != UB_OK || !bytes.Equal(payload, []byte{0x01, 0x02}) {
|
||||
t.Fatalf("seq=%d cmd=%d payload=% X, want 5 / OK / 01 02", seq, cmd, payload)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user