Skip to content

Commit e376ee8

Browse files
author
Alex J Lennon
committed
feat(backends): transport config, BackendStream, connect_backend (#9)
- Add BackendTransport (tcp | native) and connect_backend(proxy, backend_cfg) - BackendStream enum with Tcp variant; proxy session uses Framed<BackendStream> - Native returns NotImplemented until probe integration lands - Relax backend_type validation to non-empty label; CLI --transport; env RSGDB_TRANSPORT - Re-export connect_backend, BackendTransport; document in README / rsgdb.toml.example Made-with: Cursor
1 parent 8911660 commit e376ee8

11 files changed

Lines changed: 235 additions & 45 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ target_host = "localhost"
117117
target_port = 3334
118118

119119
[backend]
120-
# Used for logging / future integration; the proxy connects over TCP to target_host:target_port
120+
# Label for logs (openocd, probe-rs, gdbserver, …)
121121
backend_type = "openocd"
122+
# tcp = GDB stub on target_host:target_port; native = reserved (see GitHub #9)
123+
transport = "tcp"
122124

123125
[logging]
124126
level = "debug"
@@ -294,7 +296,7 @@ Source of truth for ordering and scope: **[GitHub Issues](https://github.com/Dyn
294296
| Milestone (docs) | What it means | Issue |
295297
|------------------|---------------|-------|
296298
| **Foundation + proxy** | RSP codec, TCP proxy, config, logging, CI (incl. GDB + Zephyr E2E), session record (JSONL), SVD labels, flash orchestration, RTOS decode/log | Closed: [#1#8](https://github.com/DynamicDevices/rsgdb/issues?q=is%3Aissue+is%3Aclosed) |
297-
| **Next: native backend** | Probe-facing backend beyond TCP to a stub (see `backends::connect_tcp_backend`) | [#9](https://github.com/DynamicDevices/rsgdb/issues/9) (open) |
299+
| **Next: native backend** | Implement `BackendTransport::Native`; default `transport = "tcp"``connect_backend` (`src/backends/mod.rs`) | [#9](https://github.com/DynamicDevices/rsgdb/issues/9) (open) |
298300
| **Replay** | `rsgdb replay` + mock TCP backend from `.jsonl` | [#10](https://github.com/DynamicDevices/rsgdb/issues/10) (closed) |
299301
| **Richer SVD** | Overlapping fields + enum variant names in annotations; value decode / recording correlation follow-ups | [#11](https://github.com/DynamicDevices/rsgdb/issues/11) (baseline closed; follow-ups optional) |
300302

rsgdb.toml.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ enable_named = true
5050
enable_conditional = true
5151

5252
[backend]
53-
# Backend type: openocd, probe-rs, pyocd
53+
# Label for logs / tooling (openocd, probe-rs, gdbserver, …)
5454
backend_type = "openocd"
55+
# How to reach the target: tcp = GDB stub on proxy.target_host:target_port; native = reserved (#9)
56+
transport = "tcp"
5557

5658
# Backend-specific options
5759
[backend.options]

src/backends/mod.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,55 @@
1-
//! Debug probe backends.
1+
//! Debug probe backends: how the proxy reaches the **target** GDB remote.
22
//!
3-
//! Today the proxy uses [`tcp::connect_tcp_backend`] to reach a GDB stub over TCP. A fuller
4-
//! backend abstraction for native probes is tracked as
5-
//! [#9](https://github.com/DynamicDevices/rsgdb/issues/9).
3+
//! - **[`BackendTransport`]** — `tcp` (default): connect to a GDB stub on `proxy.target_host` /
4+
//! `proxy.target_port`. `native` is reserved for direct probe integration ([#9](https://github.com/DynamicDevices/rsgdb/issues/9)).
5+
//! - **`backend_type`** in [`crate::config::BackendConfig`] is a **label** (openocd, probe-rs, …)
6+
//! for logging and future tooling; it does not change the wire path today.
67
8+
mod stream;
79
mod tcp;
810

9-
pub use tcp::connect_tcp_backend;
11+
pub use stream::BackendStream;
12+
pub use tcp::{connect_tcp_backend, connect_tcp_stream};
13+
14+
use crate::config::{BackendConfig, BackendTransport, ProxyConfig};
15+
use crate::error::RsgdbError;
16+
use crate::protocol::codec::GdbCodec;
17+
use tokio_util::codec::Framed;
18+
19+
/// Connect to the configured debug backend and return an RSP-framed stream.
20+
pub async fn connect_backend(
21+
proxy: &ProxyConfig,
22+
backend: &BackendConfig,
23+
) -> Result<Framed<BackendStream, GdbCodec>, RsgdbError> {
24+
match backend.transport {
25+
BackendTransport::Tcp => {
26+
let stream = connect_tcp_stream(proxy).await?;
27+
Ok(Framed::new(BackendStream::Tcp(stream), GdbCodec::new()))
28+
}
29+
BackendTransport::Native => Err(RsgdbError::NotImplemented(
30+
"native probe backend is not implemented yet; use [backend] transport = \"tcp\" \
31+
(GDB stub on target_host:target_port). See GitHub #9."
32+
.to_string(),
33+
)),
34+
}
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use super::*;
40+
use crate::config::BackendConfig;
41+
42+
#[tokio::test]
43+
async fn native_transport_is_not_implemented() {
44+
let proxy = ProxyConfig::default();
45+
let backend = BackendConfig {
46+
transport: BackendTransport::Native,
47+
..Default::default()
48+
};
49+
let err = connect_backend(&proxy, &backend).await.unwrap_err();
50+
match err {
51+
RsgdbError::NotImplemented(s) => assert!(s.contains("native")),
52+
e => panic!("expected NotImplemented, got {e:?}"),
53+
}
54+
}
55+
}

src/backends/stream.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! Byte stream to the debug target (TCP stub today; more variants later).
2+
3+
use std::io;
4+
use std::pin::Pin;
5+
use std::task::{Context, Poll};
6+
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
7+
use tokio::net::TcpStream;
8+
9+
/// Async byte stream used as the GDB remote **target** side of the proxy.
10+
#[derive(Debug)]
11+
pub enum BackendStream {
12+
/// Remote GDB stub (OpenOCD, probe-rs, gdbserver, …) over TCP.
13+
Tcp(TcpStream),
14+
}
15+
16+
impl AsyncRead for BackendStream {
17+
fn poll_read(
18+
mut self: Pin<&mut Self>,
19+
cx: &mut Context<'_>,
20+
buf: &mut ReadBuf<'_>,
21+
) -> Poll<io::Result<()>> {
22+
match &mut *self {
23+
BackendStream::Tcp(t) => Pin::new(t).poll_read(cx, buf),
24+
}
25+
}
26+
}
27+
28+
impl AsyncWrite for BackendStream {
29+
fn poll_write(
30+
mut self: Pin<&mut Self>,
31+
cx: &mut Context<'_>,
32+
buf: &[u8],
33+
) -> Poll<Result<usize, io::Error>> {
34+
match &mut *self {
35+
BackendStream::Tcp(t) => Pin::new(t).poll_write(cx, buf),
36+
}
37+
}
38+
39+
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
40+
match &mut *self {
41+
BackendStream::Tcp(t) => Pin::new(t).poll_flush(cx),
42+
}
43+
}
44+
45+
fn poll_shutdown(
46+
mut self: Pin<&mut Self>,
47+
cx: &mut Context<'_>,
48+
) -> Poll<Result<(), io::Error>> {
49+
match &mut *self {
50+
BackendStream::Tcp(t) => Pin::new(t).poll_shutdown(cx),
51+
}
52+
}
53+
}

src/backends/tcp.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
//! TCP connection to a remote GDB stub (OpenOCD, probe-rs, pyOCD, etc.).
2-
//!
3-
//! Native probe backends will plug in alongside this path; see [#9](https://github.com/DynamicDevices/rsgdb/issues/9).
42
53
use crate::config::ProxyConfig;
64
use crate::error::RsgdbError;
@@ -10,15 +8,13 @@ use tokio::net::TcpStream;
108
use tokio::time::timeout;
119
use tokio_util::codec::Framed;
1210

13-
/// Connect to `target_host:target_port` and wrap the stream with [`GdbCodec`].
14-
pub async fn connect_tcp_backend(
15-
config: &ProxyConfig,
16-
) -> Result<Framed<TcpStream, GdbCodec>, RsgdbError> {
11+
/// Establish a TCP connection to `target_host:target_port` (no codec).
12+
pub async fn connect_tcp_stream(config: &ProxyConfig) -> Result<TcpStream, RsgdbError> {
1713
let backend_addr = format!("{}:{}", config.target_host, config.target_port);
18-
let stream = if config.timeout_secs == 0 {
14+
if config.timeout_secs == 0 {
1915
TcpStream::connect(&backend_addr)
2016
.await
21-
.map_err(RsgdbError::Io)?
17+
.map_err(RsgdbError::Io)
2218
} else {
2319
timeout(
2420
Duration::from_secs(config.timeout_secs),
@@ -31,7 +27,14 @@ pub async fn connect_tcp_backend(
3127
backend_addr, config.timeout_secs
3228
))
3329
})?
34-
.map_err(RsgdbError::Io)?
35-
};
30+
.map_err(RsgdbError::Io)
31+
}
32+
}
33+
34+
/// Connect to `target_host:target_port` and wrap the stream with [`GdbCodec`].
35+
pub async fn connect_tcp_backend(
36+
config: &ProxyConfig,
37+
) -> Result<Framed<TcpStream, GdbCodec>, RsgdbError> {
38+
let stream = connect_tcp_stream(config).await?;
3639
Ok(Framed::new(stream, GdbCodec::new()))
3740
}

src/config.rs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,55 @@ pub struct BreakpointConfig {
125125
pub enable_conditional: bool,
126126
}
127127

128+
/// How the proxy reaches the debug target (stub TCP vs future native probe).
129+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
130+
#[serde(rename_all = "lowercase")]
131+
pub enum BackendTransport {
132+
/// GDB remote stub on `proxy.target_host`:`proxy.target_port` (OpenOCD, probe-rs GDB port, …).
133+
#[default]
134+
#[serde(alias = "stub")]
135+
Tcp,
136+
/// Direct probe / native integration (not implemented; reserved for #9).
137+
Native,
138+
}
139+
140+
impl BackendTransport {
141+
/// Parse from config strings (`tcp`, `stub`, `native`).
142+
pub fn parse(s: &str) -> ConfigResult<Self> {
143+
match s.trim().to_ascii_lowercase().as_str() {
144+
"tcp" | "stub" => Ok(Self::Tcp),
145+
"native" => Ok(Self::Native),
146+
"" => Err(ConfigError::InvalidValue {
147+
field: "backend.transport".to_string(),
148+
reason: "Cannot be empty".to_string(),
149+
}),
150+
other => Err(ConfigError::InvalidValue {
151+
field: "backend.transport".to_string(),
152+
reason: format!("Must be tcp or native, got: {other}"),
153+
}),
154+
}
155+
}
156+
}
157+
158+
impl std::str::FromStr for BackendTransport {
159+
type Err = String;
160+
161+
fn from_str(s: &str) -> Result<Self, Self::Err> {
162+
Self::parse(s).map_err(|e| e.to_string())
163+
}
164+
}
165+
128166
/// Backend configuration
129167
#[derive(Debug, Clone, Serialize, Deserialize)]
130168
pub struct BackendConfig {
131-
/// Backend type (openocd, probe-rs, pyocd)
169+
/// Label for the stub or tool (openocd, probe-rs, pyocd, …) — logging / future use only.
132170
#[serde(default = "default_backend_type")]
133171
pub backend_type: String,
134172

173+
/// Transport to the target (`tcp` = remote stub; `native` reserved).
174+
#[serde(default)]
175+
pub transport: BackendTransport,
176+
135177
/// Backend-specific options
136178
#[serde(default)]
137179
pub options: std::collections::HashMap<String, String>,
@@ -235,6 +277,7 @@ impl Default for BackendConfig {
235277
fn default() -> Self {
236278
Self {
237279
backend_type: default_backend_type(),
280+
transport: BackendTransport::default(),
238281
options: std::collections::HashMap::new(),
239282
}
240283
}
@@ -326,12 +369,11 @@ impl Config {
326369
});
327370
}
328371

329-
// Validate backend config
330-
let valid_backends = ["openocd", "probe-rs", "pyocd"];
331-
if !valid_backends.contains(&self.backend.backend_type.as_str()) {
372+
// Backend label is free-form (openocd, probe-rs, custom); must not be empty.
373+
if self.backend.backend_type.trim().is_empty() {
332374
return Err(ConfigError::InvalidValue {
333375
field: "backend.backend_type".to_string(),
334-
reason: format!("Must be one of: {}", valid_backends.join(", ")),
376+
reason: "Cannot be empty".to_string(),
335377
});
336378
}
337379

@@ -382,6 +424,12 @@ impl Config {
382424
self.backend.backend_type = backend;
383425
}
384426

427+
if let Ok(t) = std::env::var("RSGDB_TRANSPORT") {
428+
if let Ok(tr) = BackendTransport::parse(&t) {
429+
self.backend.transport = tr;
430+
}
431+
}
432+
385433
if let Ok(v) = std::env::var("RSGDB_RECORD") {
386434
let v = v.trim();
387435
if v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes") {

src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ pub mod scaffold;
3737
pub mod svd;
3838

3939
// Stable names for scaffold submodules (see `scaffold` module docs).
40-
pub use scaffold::breakpoints;
41-
pub use scaffold::state;
42-
pub use scaffold::ui;
40+
pub use scaffold::{breakpoints, state, ui};
41+
42+
pub use crate::backends::connect_backend;
4343

4444
// Re-export commonly used types
45-
pub use config::{Config, FlashConfig, SvdConfig};
45+
pub use config::{BackendConfig, BackendTransport, Config, FlashConfig, SvdConfig};
4646
pub use error::{Result, RsgdbError};
4747
pub use logging_setup::{init_from_logging_config, LoggingInitGuard};
4848

src/main.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use anyhow::Context;
66
use clap::{Parser, Subcommand};
7-
use rsgdb::config::Config;
7+
use rsgdb::config::{BackendTransport, Config};
88
use rsgdb::flash;
99
use rsgdb::proxy::ProxyServer;
1010
use rsgdb::svd::SvdIndex;
@@ -92,6 +92,10 @@ struct ProxyArgs {
9292
/// CMSIS-SVD file path (peripheral/register labels for memory RSP in logs)
9393
#[arg(long, value_name = "FILE")]
9494
svd: Option<PathBuf>,
95+
96+
/// Backend transport: `tcp` (GDB stub on target_host:target_port) or `native` (not implemented)
97+
#[arg(long, value_name = "tcp|native")]
98+
transport: Option<String>,
9599
}
96100

97101
#[tokio::main]
@@ -195,6 +199,10 @@ async fn run_proxy(args: ProxyArgs) -> anyhow::Result<()> {
195199
if let Some(backend) = args.backend {
196200
config.backend.backend_type = backend;
197201
}
202+
if let Some(ref t) = args.transport {
203+
config.backend.transport =
204+
BackendTransport::parse(t).map_err(|e| anyhow::anyhow!("{}", e))?;
205+
}
198206

199207
if args.record {
200208
config.recording.enabled = true;
@@ -242,13 +250,19 @@ async fn run_proxy(args: ProxyArgs) -> anyhow::Result<()> {
242250

243251
info!(
244252
backend_type = %config.backend.backend_type,
245-
"Configured debug backend (for future integration)"
253+
transport = ?config.backend.transport,
254+
"Configured debug backend"
246255
);
247256

248257
info!("Configuration: {:?}", config);
249258

250-
let mut server =
251-
ProxyServer::new(config.proxy.clone(), config.recording.clone(), svd_index).await?;
259+
let mut server = ProxyServer::new(
260+
config.proxy.clone(),
261+
config.backend.clone(),
262+
config.recording.clone(),
263+
svd_index,
264+
)
265+
.await?;
252266

253267
info!(
254268
listen = %server.local_addr()?,

0 commit comments

Comments
 (0)