This repository has been archived on 2026-08-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
donglora/clients/rust/src/connect.rs
T
swaits 9aeec80d7a 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
2026-04-06 14:00:42 -06:00

175 lines
5.7 KiB
Rust

//! Connection auto-detection and mux client helpers.
//!
//! The [`connect`] function tries mux connections first (TCP via env var, then
//! Unix socket), falling back to direct USB serial. This matches the Python
//! client's `connect()` behavior.
use std::time::Duration;
use tracing::debug;
use crate::client::Client;
use crate::discovery;
use crate::transport::{AnyTransport, MuxTransport, SerialTransport};
/// Default read timeout for connections.
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
/// Resolve the mux socket path in priority order.
///
/// 1. `$DONGLORA_MUX` environment variable
/// 2. `$XDG_RUNTIME_DIR/donglora/mux.sock`
/// 3. `/tmp/donglora-mux.sock`
pub fn default_socket_path() -> String {
if let Ok(env) = std::env::var("DONGLORA_MUX") {
return env;
}
if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
return format!("{xdg}/donglora/mux.sock");
}
"/tmp/donglora-mux.sock".to_string()
}
/// Find an existing mux socket path, or `None` if no socket file exists.
fn find_mux_socket() -> Option<String> {
if let Ok(env) = std::env::var("DONGLORA_MUX") {
if std::path::Path::new(&env).exists() {
return Some(env);
}
return None;
}
if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
let p = format!("{xdg}/donglora/mux.sock");
if std::path::Path::new(&p).exists() {
return Some(p);
}
}
let p = "/tmp/donglora-mux.sock";
if std::path::Path::new(p).exists() {
return Some(p.to_string());
}
None
}
/// Connect to the mux daemon via Unix domain socket.
#[cfg(unix)]
pub fn mux_connect(
path: Option<&str>,
timeout: Duration,
) -> anyhow::Result<Client<MuxTransport>> {
let path = match path {
Some(p) => p.to_string(),
None => find_mux_socket()
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
};
let transport = MuxTransport::unix(&path, timeout)?;
Ok(Client::new(transport))
}
/// Connect to the mux daemon via TCP.
pub fn mux_tcp_connect(
host: &str,
port: u16,
timeout: Duration,
) -> anyhow::Result<Client<MuxTransport>> {
let transport = MuxTransport::tcp(host, port, timeout)?;
Ok(Client::new(transport))
}
/// Auto-detect and connect to a DongLoRa device.
///
/// Priority:
/// 1. `DONGLORA_MUX_TCP` env var → TCP mux connection
/// 2. Unix socket mux (if socket file exists)
/// 3. Direct USB serial (auto-detect by VID:PID, blocks until device appears)
///
/// If `port` is `Some`, skips mux detection and connects directly to that serial port.
pub fn connect(
port: Option<&str>,
timeout: Duration,
) -> anyhow::Result<Client<AnyTransport>> {
// If explicit port given, go direct
if let Some(port) = port {
debug!("opening serial port {port}");
let transport = SerialTransport::open(port, timeout)?;
return Ok(Client::new(AnyTransport::Serial(transport)));
}
// 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)]
if let Some(path) = find_mux_socket() {
match MuxTransport::unix(&path, timeout) {
Ok(transport) => {
debug!("connected to mux socket at {path}");
return Ok(Client::new(AnyTransport::Mux(transport)));
}
Err(_) => {
// Stale socket or connection refused — fall through
debug!("mux socket at {path} not reachable, falling back to USB");
}
}
}
// Direct USB serial — auto-detect or wait
let port_path = discovery::find_port()
.unwrap_or_else(discovery::wait_for_device);
debug!("opening serial port {port_path}");
let transport = SerialTransport::open(&port_path, timeout)?;
Ok(Client::new(AnyTransport::Serial(transport)))
}
/// Convenience: connect with default timeout.
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 };
let port: u16 = p.parse().ok()?;
(host.to_string(), port)
} else {
let port: u16 = addr.parse().ok()?;
("localhost".to_string(), port)
};
MuxTransport::tcp(&host, port, timeout).ok()
}