fix: prevent client from stealing serial port when mux is restarting

Track mux usage with a static AtomicBool. Once a mux connection
succeeds, all future connect() calls in the same process only try
the mux. connect() blocks waiting for the socket to reappear;
try_connect() returns an error so callers can retry with backoff.
This commit is contained in:
swaits committed 2026-04-11 02:50:49 -06:00
1 parent 77addfa698
commit 055fc76b87
2 files changed
+71 -33

No files matched your search

+9
View File
@@ -1,5 +1,14 @@
# Changelog
## Unreleased
### Fixed
- Once a mux connection succeeds, all future `connect()` calls in the same
process only try the mux. `connect()` blocks waiting for the socket to
reappear; `try_connect()` returns an error so callers can retry with backoff.
Prevents clients from stealing the serial port during mux restarts.
## 0.2.1 — 2026-04-07
### Fixed
+62 -33
View File
@@ -4,14 +4,19 @@
//! Unix socket), falling back to direct USB serial. This matches the Python
//! client's `connect()` behavior.
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tracing::debug;
use tracing::{debug, info};
use crate::client::Client;
use crate::discovery;
use crate::transport::{AnyTransport, MuxTransport, SerialTransport};
/// Set once this process connects via mux. All future `connect()` calls
/// will only try mux, never fall through to direct serial.
static USED_MUX: AtomicBool = AtomicBool::new(false);
/// Default read timeout for connections.
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
@@ -90,29 +95,42 @@ pub fn connect(port: Option<&str>, timeout: Duration) -> anyhow::Result<Client<A
return Ok(client);
}
// 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}");
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// Try Unix socket mux — if a mux socket exists, commit to it. Never fall through
// to USB serial when a mux is (or was) running. This prevents a client from
// stealing the serial port while the mux is temporarily reconnecting.
#[cfg(unix)]
if let Some(path) = find_mux_socket() {
debug!("mux socket found at {path} — connecting via mux only");
// Previously connected via mux — stay on mux, wait if necessary
if USED_MUX.load(Ordering::Relaxed) {
let path = default_socket_path();
while !std::path::Path::new(&path).exists() {
info!("Waiting for mux at {path} ...");
std::thread::sleep(Duration::from_millis(500));
}
let transport = MuxTransport::unix(&path, timeout)?;
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// Direct USB serial — only reached when no mux socket exists at all
// 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}");
USED_MUX.store(true, Ordering::Relaxed);
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// Try Unix socket mux — if a mux socket exists, commit to it
#[cfg(unix)]
if let Some(path) = find_mux_socket() {
debug!("mux socket found at {path} — connecting via mux only");
USED_MUX.store(true, Ordering::Relaxed);
let transport = MuxTransport::unix(&path, timeout)?;
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// No mux found on first attempt — direct USB serial
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)?;
@@ -166,27 +184,38 @@ pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport
/// socket mux, then direct USB serial — but uses a single non-blocking scan
/// instead of polling indefinitely for a USB device.
pub fn try_connect(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}");
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// Try Unix socket mux — commit to it if socket exists (same rationale as connect())
#[cfg(unix)]
if let Some(path) = find_mux_socket() {
debug!("mux socket found at {path} — connecting via mux only");
// Previously connected via mux — only try mux
if USED_MUX.load(Ordering::Relaxed) {
let path = find_mux_socket().ok_or_else(|| anyhow::anyhow!("mux not available (waiting for restart)"))?;
let transport = MuxTransport::unix(&path, timeout)?;
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// Direct USB serial — single non-blocking scan (only when no mux socket exists)
// 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}");
USED_MUX.store(true, Ordering::Relaxed);
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// Try Unix socket mux — commit to it if socket exists
#[cfg(unix)]
if let Some(path) = find_mux_socket() {
debug!("mux socket found at {path} — connecting via mux only");
USED_MUX.store(true, Ordering::Relaxed);
let transport = MuxTransport::unix(&path, timeout)?;
let mut client = Client::new(AnyTransport::Mux(transport));
client.validate()?;
return Ok(client);
}
// No mux found on first attempt — direct USB serial (single non-blocking scan)
let port_path =
discovery::find_port().ok_or_else(|| anyhow::anyhow!("no DongLoRa device found (no mux, no USB device)"))?;
debug!("opening serial port {port_path}");