fix(clients/rust): harden client resilience for v0.1.0 release

- Track serial timeout in field instead of returning hardcoded 2s
- Always restore timeout in drain_rx, even on read errors
- Handle EINTR/Interrupted in read_frame (retry instead of error)
- Use connect_timeout for TCP mux (no more 2-min hangs)
- Set write timeout on mux sockets (unix + tcp)
- Log unexpected unsolicited frames in recv via tracing::warn
- Add unix socket timeout test for WouldBlock handling
- Add CHANGELOG.md, remove resolved CLAUDE_TODO.md
This commit is contained in:
swaits committed 2026-04-06 14:00:42 -06:00
1 parent 0676bd4606
commit 7dd717a887
6 files changed
+158 -23

No files matched your search

+30
View File
@@ -131,6 +131,36 @@ pub fn connect_default() -> anyhow::Result<Client<AnyTransport>> {
connect(None, DEFAULT_TIMEOUT)
}
/// Connect to a mux daemon only (TCP via env var, then Unix socket).
///
/// Unlike [`connect`], this **never** falls back to direct USB serial.
/// Returns an error if no mux is reachable — the caller can retry with backoff.
///
/// This is the Rust equivalent of the Python client's "sticky mux" behavior:
/// once you decide to use the mux, you stay on the mux.
pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
// Try TCP mux via environment variable.
if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
{
debug!("connected to TCP mux at {tcp}");
return Ok(Client::new(AnyTransport::Mux(transport)));
}
// Try Unix socket mux.
#[cfg(unix)]
{
let path = find_mux_socket()
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
let transport = MuxTransport::unix(&path, timeout)?;
debug!("connected to mux socket at {path}");
Ok(Client::new(AnyTransport::Mux(transport)))
}
#[cfg(not(unix))]
anyhow::bail!("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP")
}
fn try_tcp_mux(addr: &str, timeout: Duration) -> Option<MuxTransport> {
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
let host = if h.is_empty() { "localhost" } else { h };