Bug
SerialConnection.connect() in serial_cx.py leaks file descriptors when a connection attempt times out. Each failed attempt leaves the serial port fd and two internal pipe fds open. In a reconnect loop (e.g. RemoteTerm's 5-second retry), this exhausts the process fd limit (typically 1024) over time, causing both serial reconnection and HTTP request handling to fail with OSError: [Errno 24] Too many open files.
Root cause
# serial_cx.py — current code
async def connect(self, timeout: float = 10.0):
self._connected_event.clear()
loop = asyncio.get_running_loop()
await serial_asyncio.create_serial_connection( # opens serial fd + 2 pipe fds
loop,
lambda: self.MCSerialClientProtocol(self),
self.port,
baudrate=self.baudrate,
)
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
# ^ if this raises TimeoutError, nothing above gets closed
When _connected_event is not set within timeout seconds (radio connected at USB level but not responding to the protocol handshake), asyncio.wait_for raises TimeoutError. Because self.transport is only set inside the connection_made protocol callback — which may not have fired yet — disconnect() is a no-op (self.transport is None), and the fds opened by create_serial_connection are never released.
Proposed fix
Capture the transport directly from create_serial_connection and close it on any exception:
async def connect(self, timeout: float = 10.0):
self._connected_event.clear()
loop = asyncio.get_running_loop()
transport, _ = await serial_asyncio.create_serial_connection(
loop,
lambda: self.MCSerialClientProtocol(self),
self.port,
baudrate=self.baudrate,
)
try:
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
except Exception:
transport.close()
raise
logger.info("Serial Connection started")
return self.port
How to reproduce
- Connect a MeshCore radio via USB
- Start a process that connects via
MeshCore.create_serial() and retries on failure
- Unplug the radio (or use a device that enumerates but doesn't respond to the handshake)
- Watch
lsof -p <pid> | grep ttyACM | wc -l climb by ~3 every retry cycle
- After ~340 retries the process hits 1024 open fds and all I/O fails
Environment
meshcore 2.3.7
- Python 3.13
- Linux / serial over USB (ACM device)
- Discovered via RemoteTerm for MeshCore running as a long-lived systemd service
Happy to submit a PR with the fix if that's helpful.
Bug
SerialConnection.connect()inserial_cx.pyleaks file descriptors when a connection attempt times out. Each failed attempt leaves the serial port fd and two internal pipe fds open. In a reconnect loop (e.g. RemoteTerm's 5-second retry), this exhausts the process fd limit (typically 1024) over time, causing both serial reconnection and HTTP request handling to fail withOSError: [Errno 24] Too many open files.Root cause
When
_connected_eventis not set withintimeoutseconds (radio connected at USB level but not responding to the protocol handshake),asyncio.wait_forraisesTimeoutError. Becauseself.transportis only set inside theconnection_madeprotocol callback — which may not have fired yet —disconnect()is a no-op (self.transport is None), and the fds opened bycreate_serial_connectionare never released.Proposed fix
Capture the transport directly from
create_serial_connectionand close it on any exception:How to reproduce
MeshCore.create_serial()and retries on failurelsof -p <pid> | grep ttyACM | wc -lclimb by ~3 every retry cycleEnvironment
meshcore2.3.7Happy to submit a PR with the fix if that's helpful.