fix: prevent client from stealing serial port when mux is restarting
Track mux usage with a module-level flag. 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() raises immediately so callers can retry with backoff.
This commit is contained in:
2 files changed
+49
-10
No files matched your search
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Once a mux connection succeeds, all future `connect()` calls in the
|
||||
same process only try the mux (waiting for it to reappear if needed).
|
||||
`try_connect()` raises immediately so callers can retry with backoff.
|
||||
Prevents clients from stealing the serial port during mux restarts.
|
||||
|
||||
## 0.2.0 — 2026-04-08
|
||||
|
||||
### Features
|
||||
|
||||
+40
-10
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import serial
|
||||
@@ -55,6 +56,11 @@ def _find_mux_socket() -> str | None:
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
_used_mux = False
|
||||
"""Set once this process connects via mux. All future connect() calls
|
||||
will only try mux, never fall through to direct serial."""
|
||||
|
||||
|
||||
def _try_tcp_mux(addr: str, timeout: float) -> MuxConnection | None:
|
||||
"""Try to connect to a TCP mux at *addr* (``[host:]port``).
|
||||
|
||||
@@ -104,7 +110,7 @@ def mux_tcp_connect(host: str, port: int, timeout: float = DEFAULT_TIMEOUT) -> M
|
||||
def connect(port: str | None = None, timeout: float = DEFAULT_TIMEOUT) -> Any:
|
||||
"""Auto-detect and connect to a DongLoRa device.
|
||||
|
||||
Priority:
|
||||
Priority (first connection):
|
||||
|
||||
1. ``DONGLORA_MUX_TCP`` env var -> TCP mux connection
|
||||
2. Unix socket mux (if socket file exists)
|
||||
@@ -112,12 +118,12 @@ def connect(port: str | None = None, timeout: float = DEFAULT_TIMEOUT) -> Any:
|
||||
|
||||
If *port* is given, skips mux detection and connects directly.
|
||||
|
||||
.. note::
|
||||
|
||||
If a mux socket file exists, this function **commits** to the mux
|
||||
and will raise on failure rather than falling through to USB serial.
|
||||
This prevents port-stealing race conditions (matches Rust v0.2.1).
|
||||
Once a mux connection succeeds, all future calls in this process only
|
||||
try the mux (waiting for it to reappear if necessary). This prevents
|
||||
clients from stealing the serial port during a mux restart.
|
||||
"""
|
||||
global _used_mux
|
||||
|
||||
# Explicit port — go direct
|
||||
if port is not None:
|
||||
log.debug("opening serial port %s", port)
|
||||
@@ -126,21 +132,33 @@ def connect(port: str | None = None, timeout: float = DEFAULT_TIMEOUT) -> Any:
|
||||
validate(ser)
|
||||
return ser
|
||||
|
||||
# Previously connected via mux — stay on mux, wait if necessary
|
||||
if _used_mux:
|
||||
mux_path = default_socket_path()
|
||||
while not os.path.exists(mux_path):
|
||||
log.info("Waiting for mux at %s ...", mux_path)
|
||||
time.sleep(0.5)
|
||||
conn = mux_connect(mux_path, timeout)
|
||||
return conn
|
||||
|
||||
# Try TCP mux via environment variable
|
||||
tcp = os.environ.get("DONGLORA_MUX_TCP")
|
||||
if tcp:
|
||||
conn = _try_tcp_mux(tcp, timeout)
|
||||
if conn is not None:
|
||||
log.debug("connected to TCP mux at %s", tcp)
|
||||
_used_mux = True
|
||||
return conn
|
||||
|
||||
# Try Unix socket mux — commit if socket exists (no fall-through)
|
||||
sock_path = _find_mux_socket()
|
||||
if sock_path is not None:
|
||||
log.debug("mux socket found at %s — connecting via mux only", sock_path)
|
||||
return mux_connect(sock_path, timeout)
|
||||
conn = mux_connect(sock_path, timeout)
|
||||
_used_mux = True
|
||||
return conn
|
||||
|
||||
# Direct USB serial — only reached when no mux socket exists at all
|
||||
# No mux found on first attempt — direct USB serial
|
||||
port_path = find_port()
|
||||
if port_path is None:
|
||||
port_path = wait_for_device()
|
||||
@@ -187,22 +205,34 @@ def try_connect(timeout: float = DEFAULT_TIMEOUT) -> Any:
|
||||
Runs the same fallback chain (TCP mux, Unix socket mux, direct USB
|
||||
serial) but uses a single non-blocking scan instead of polling
|
||||
indefinitely for a USB device. Raises if nothing is found.
|
||||
|
||||
If a previous call connected via mux, only tries mux (raises if
|
||||
the mux socket is not currently available).
|
||||
"""
|
||||
global _used_mux
|
||||
|
||||
# Previously connected via mux — only try mux
|
||||
if _used_mux:
|
||||
return mux_connect(None, timeout)
|
||||
|
||||
# Try TCP mux via environment variable
|
||||
tcp = os.environ.get("DONGLORA_MUX_TCP")
|
||||
if tcp:
|
||||
conn = _try_tcp_mux(tcp, timeout)
|
||||
if conn is not None:
|
||||
log.debug("connected to TCP mux at %s", tcp)
|
||||
_used_mux = True
|
||||
return conn
|
||||
|
||||
# Try Unix socket mux — commit if socket exists
|
||||
sock_path = _find_mux_socket()
|
||||
if sock_path is not None:
|
||||
log.debug("mux socket found at %s — connecting via mux only", sock_path)
|
||||
return mux_connect(sock_path, timeout)
|
||||
conn = mux_connect(sock_path, timeout)
|
||||
_used_mux = True
|
||||
return conn
|
||||
|
||||
# Direct USB serial — single non-blocking scan
|
||||
# No mux found on first attempt — direct USB serial (single non-blocking scan)
|
||||
port_path = find_port()
|
||||
if port_path is None:
|
||||
raise FileNotFoundError("no DongLoRa device found (no mux, no USB device)")
|
||||
|
||||
Reference in new issue
Block a user