Fix live status on CS2: accept RCON output tagged with the end packet's id

CS2 tags a command's output with the id of the empty end packet when both
arrive together, and replies to the end packet with a bare \x00\x01. The
client dropped the output as stale, so the panel reported WebPanelBridge as
missing. Also log each new kind of poll failure once, and recovery.
This commit is contained in:
Astra 2026-09-25 21:27:10 +01:00
parent 4f38daf0e6
commit 10694c69bc
3 changed files with 63 additions and 15 deletions

View file

@ -30,7 +30,7 @@ func TestArg(t *testing.T) {
// fakeServer answers auth, then replies to each command with its text split over two packets,
// and mirrors the empty end-marker packet the way srcds does.
func fakeServer(t *testing.T, password string) string {
func fakeServer(t *testing.T, password string, cs2 bool) string {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
@ -42,7 +42,7 @@ func fakeServer(t *testing.T, password string) string {
if err != nil {
return
}
go serve(c, password)
go serve(c, password, cs2)
}
}()
return ln.Addr().String()
@ -69,8 +69,9 @@ func writePkt(w io.Writer, id, typ int32, body string) {
w.Write(buf)
}
func serve(c net.Conn, password string) {
func serve(c net.Conn, password string, cs2 bool) {
defer c.Close()
pending := ""
for {
id, typ, body, err := readPkt(c)
if err != nil {
@ -85,10 +86,20 @@ func serve(c net.Conn, password string) {
writePkt(c, -1, typeAuthResponse, "")
}
case typeExecCommand:
if cs2 {
// CS2 answers after reading the end packet too, tagging the output with its id.
pending = "echo:" + body + "\n"
continue
}
out := "echo:" + body + "\n" + strings.Repeat("x", 5000)
writePkt(c, id, typeResponseValue, out[:4000])
writePkt(c, id, typeResponseValue, out[4000:])
case typeResponseValue:
if cs2 {
writePkt(c, id, typeResponseValue, pending)
writePkt(c, id, typeResponseValue, "\x00\x01")
continue
}
writePkt(c, id, typeResponseValue, "")
writePkt(c, id, typeResponseValue, "\x00\x01\x00\x00")
}
@ -96,7 +107,7 @@ func serve(c net.Conn, password string) {
}
func TestExecMultiPacket(t *testing.T) {
addr := fakeServer(t, "pw")
addr := fakeServer(t, "pw", false)
c := New(addr, "pw")
defer c.Close()
for i := 0; i < 3; i++ {
@ -111,7 +122,7 @@ func TestExecMultiPacket(t *testing.T) {
}
func TestWrongPassword(t *testing.T) {
addr := fakeServer(t, "pw")
addr := fakeServer(t, "pw", false)
c := New(addr, "nope")
if _, err := c.Exec("status"); err != ErrAuth {
t.Fatalf("got %v, want ErrAuth", err)
@ -130,3 +141,18 @@ func TestBackoff(t *testing.T) {
t.Fatalf("second call should fail fast with ErrDown, got %v", err)
}
}
// CS2 tags output with the end packet's id and ends with a bare "\x00\x01" (seen live on fr04).
func TestExecCS2Ids(t *testing.T) {
c := New(fakeServer(t, "pw", true), "pw")
defer c.Close()
for i := 0; i < 3; i++ {
out, err := c.Exec("css_webpanel_status")
if err != nil {
t.Fatal(err)
}
if out != "echo:css_webpanel_status\n" {
t.Fatalf("got %q", out)
}
}
}