Skip to content

Commit f4a7e22

Browse files
author
Alex J Lennon
committed
feat(phase-b): backend thread reply summaries, gdbinit example, qSupported proxy test
- Add summarize_backend_thread_payload for backend→GDB thread RSP (l/m/QC/hex names) - Log summaries at rsgdb::rtos when proxy forwards backend thread replies - Add scripts/gdbinit.rsgdb.example for GDB ergonomics - Add proxy integration test for qSupported-style negotiation round-trip - Document Phase B in README and CONTRIBUTING Made-with: Cursor
1 parent 14226af commit f4a7e22

7 files changed

Lines changed: 152 additions & 0 deletions

File tree

CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ Codec framing + proxy TCP integration tests only (~1s):
5656

5757
Use this when iterating on `src/protocol/codec.rs` or `tests/proxy_integration.rs` without running the full `cargo test` suite.
5858

59+
### Phase B — GDB snippet + thread reply logging
60+
61+
- Copy or source [`scripts/gdbinit.rsgdb.example`](scripts/gdbinit.rsgdb.example) when connecting GDB through rsgdb (adjust `target extended-remote` to your listen port).
62+
- Backend **thread-related** RSP replies (`m…` / `l` / `QC…` / hex thread names) get short summaries at **`rsgdb::rtos`** (debug); packets are unchanged on the wire.
63+
5964
### Simulated GDB session (optional, Linux/macOS)
6065

6166
End-to-end smoke: **gdbserver → rsgdb → GDB** (batch), same shape as CI job **E2E GDB smoke**.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ A modern, feature-rich GDB server/proxy written in Rust, designed to enhance emb
3030
- 🧵 **RTOS RSP decode / log (Zephyr-first)** — thread-extension packets are decoded and logged at `target: rsgdb::rtos` (debug). Thread *data* comes from your stub (e.g. OpenOCD **Zephyr** RTOS awareness); other RTOSes use the same GDB RSP when the stub implements them (see below).
3131
- 🧪 **CI + local E2E smoke**`gdbserver``rsgdb``gdb` (batch), script `scripts/e2e_gdb_smoke.sh`; GitHub Actions job **E2E GDB smoke** (Ubuntu). See [CONTRIBUTING.md](CONTRIBUTING.md).
3232
-**Phase A (trust path)** — RSP codec matrix tests (`tests/rsp_codec_matrix.rs`, `scripts/e2e_rsp_regression.sh`), proxy TCP tests, ops matrix in README above.
33+
- 📎 **Phase B (GDB productivity)**[`scripts/gdbinit.rsgdb.example`](scripts/gdbinit.rsgdb.example), `qSupported`-style proxy test, backend thread-reply summaries in `rsgdb::rtos` (decode/log only).
3334

3435
### Planned
3536
- 📊 Enhanced logging with filtering and export (JSON, CSV)
@@ -87,6 +88,14 @@ rsgdb is a **transparent TCP proxy**: GDB speaks RSP to rsgdb; rsgdb forwards th
8788

8889
**Fast RSP regression (no gdb binary):** `./scripts/e2e_rsp_regression.sh` runs codec + proxy integration tests only.
8990

91+
### GDB productivity (Phase B)
92+
93+
- **Example GDB init:** [`scripts/gdbinit.rsgdb.example`](scripts/gdbinit.rsgdb.example)`pagination off`, optional `set debug remote 1`, and a commented `target extended-remote` line. Copy or `gdb -x scripts/gdbinit.rsgdb.example` after adjusting the port.
94+
- **Transparency:** integration tests include a **`qSupported:…`-style** packet round-trip so negotiation-shaped payloads are not mangled by the proxy.
95+
- **Thread replies (read-only logs):** when the stub sends thread-list / `QC` / hex name replies, **`rsgdb::rtos`** logs a short summary for **backend → client** packets (same wire bytes; no RSP injection).
96+
97+
Enable with e.g. `RUST_LOG=rsgdb::rtos=debug,rsgdb=info`.
98+
9099
### Configuration
91100

92101
Create a `rsgdb.toml` configuration file:
@@ -242,6 +251,7 @@ rsgdb/
242251
├── scripts/e2e_gdb_smoke.sh # gdbserver → rsgdb → gdb (batch); CI E2E job
243252
├── scripts/e2e_zephyr_native_sim.sh # optional: west build native_sim + multi-printf stepping test
244253
├── scripts/e2e_rsp_regression.sh # fast: codec + proxy integration only (no gdb)
254+
├── scripts/gdbinit.rsgdb.example # optional GDB init snippet for use with rsgdb
245255
├── scripts/zephyr_multi_printf_app/ # tiny Zephyr app for that script (west -s)
246256
├── rsgdb.toml.example
247257
└── .github/workflows/

scripts/gdbinit.rsgdb.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Example GDB init for use with rsgdb (copy to ~/.gdbinit or use: gdb -x scripts/gdbinit.rsgdb.example)
2+
# rsgdb listens for GDB; your stub (OpenOCD, gdbserver, …) listens on target_host:target_port.
3+
4+
set pagination off
5+
set confirm off
6+
7+
# Optional: see raw RSP traffic in GDB (verbose; use when debugging proxy vs stub issues)
8+
# set debug remote 1
9+
10+
# Typical connection (adjust host/port to match rsgdb --port)
11+
# target extended-remote localhost:3333

src/proxy/server.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,13 @@ impl ProxySession {
377377
"stop reply"
378378
);
379379
}
380+
} else if let Some(note) = rtos::summarize_backend_thread_payload(&p.data) {
381+
tracing::debug!(
382+
target: "rsgdb::rtos",
383+
direction = "backend_to_client",
384+
summary = %note,
385+
"backend thread reply"
386+
);
380387
}
381388
}
382389

src/rtos/backend_reply.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Parse **backend → GDB** RSP payloads for read-only logging (Phase B). Does not modify packets.
2+
3+
/// Summarize thread-related **reply** payloads (not `T` stop replies — use [`super::summarize_stop_reply`]).
4+
pub fn summarize_backend_thread_payload(data: &[u8]) -> Option<String> {
5+
if data.is_empty() {
6+
return None;
7+
}
8+
// Stop replies are handled separately (may contain binary).
9+
if data.first() == Some(&b'T') {
10+
return None;
11+
}
12+
13+
let s = std::str::from_utf8(data).ok()?;
14+
15+
if s == "l" {
16+
return Some("thread list: end (l)".to_string());
17+
}
18+
19+
if let Some(rest) = s.strip_prefix('m') {
20+
if rest.is_empty() {
21+
return Some("thread list: m (empty)".to_string());
22+
}
23+
let n = rest.split(',').filter(|seg| !seg.is_empty()).count();
24+
return Some(format!("thread list: {n} id(s)"));
25+
}
26+
27+
if let Some(rest) = s.strip_prefix("QC") {
28+
return Some(format!("current thread (qC reply): QC{rest}"));
29+
}
30+
31+
// qThreadExtraInfo reply: hex-encoded UTF-8 name (ASCII hex digits only).
32+
if data.len() >= 2 && data.len() % 2 == 0 && data.iter().all(|b| b.is_ascii_hexdigit()) {
33+
if let Ok(bytes) = hex::decode(data) {
34+
if let Ok(text) = String::from_utf8(bytes) {
35+
if text.len() <= 512 {
36+
return Some(format!("thread extra info (decoded): {text:?}"));
37+
}
38+
}
39+
}
40+
}
41+
42+
None
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
49+
#[test]
50+
fn thread_list_end() {
51+
assert_eq!(
52+
summarize_backend_thread_payload(b"l").as_deref(),
53+
Some("thread list: end (l)")
54+
);
55+
}
56+
57+
#[test]
58+
fn thread_list_ids() {
59+
let s = summarize_backend_thread_payload(b"m1,2,a").expect("summary");
60+
assert!(s.contains("3 id"), "{s}");
61+
}
62+
63+
#[test]
64+
fn current_thread_qc() {
65+
let s = summarize_backend_thread_payload(b"QC1").expect("summary");
66+
assert!(s.contains("QC1"), "{s}");
67+
}
68+
69+
#[test]
70+
fn thread_name_hex() {
71+
// "main" as UTF-8 hex: 6d61696e
72+
let s = summarize_backend_thread_payload(b"6d61696e").expect("summary");
73+
assert!(s.contains("main"), "{s}");
74+
}
75+
76+
#[test]
77+
fn t_packet_not_handled_here() {
78+
assert!(summarize_backend_thread_payload(b"T05").is_none());
79+
}
80+
}

src/rtos/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
//! `zephyr` RTOS awareness or equivalent). The wire protocol is standard GDB RSP; FreeRTOS, ThreadX, etc.
55
//! use the same packet shapes when the stub implements them.
66
7+
mod backend_reply;
8+
9+
pub use backend_reply::summarize_backend_thread_payload;
10+
711
/// Summarize a stop-reply `T…` packet for logging (often includes `thread:…` from RTOS-aware stubs).
812
pub fn summarize_stop_reply(data: &[u8]) -> Option<String> {
913
let s = std::str::from_utf8(data).ok()?;

tests/proxy_integration.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,41 @@ async fn proxy_forwards_rsp_packet_to_backend_and_back() {
103103
run.abort();
104104
}
105105

106+
/// Feature negotiation shape (`qSupported:…`) — must round-trip unchanged (Phase B transparency).
107+
#[tokio::test]
108+
async fn proxy_round_trips_qsupported_style_negotiation() {
109+
let backend = TcpListener::bind("127.0.0.1:0")
110+
.await
111+
.expect("bind backend");
112+
let backend_port = backend.local_addr().expect("backend addr").port();
113+
114+
let backend_task = tokio::spawn(echo_backend_accept_loop(backend));
115+
let (proxy_listen, run) = setup_proxy_to_backend(backend_port).await;
116+
117+
let client = tokio::net::TcpStream::connect(connect_addr(proxy_listen))
118+
.await
119+
.expect("connect to proxy");
120+
let mut client = Framed::new(client, GdbCodec::new());
121+
122+
let payload = b"qSupported:multiprocess+;xmlRegisters=i386;swbreak+;hwbreak+".as_slice();
123+
let pkt = Packet::new(payload.to_vec());
124+
client.send(PacketOrAck::Packet(pkt)).await.expect("send");
125+
126+
let got = client
127+
.next()
128+
.await
129+
.expect("stream ended")
130+
.expect("decode ok");
131+
match got {
132+
PacketOrAck::Packet(p) => assert_eq!(p.data.as_slice(), payload),
133+
other => panic!("expected packet, got {:?}", other),
134+
}
135+
136+
drop(client);
137+
backend_task.abort();
138+
run.abort();
139+
}
140+
106141
/// GDB "last signal" query: `$?#3f` — exercises checksum path used by real sessions.
107142
#[tokio::test]
108143
async fn proxy_round_trips_last_signal_query_packet() {

0 commit comments

Comments
 (0)