Skip to content

Serial fd leak in SerialConnection.connect() when connection times out #95

Description

@SpokaneMesh

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

  1. Connect a MeshCore radio via USB
  2. Start a process that connects via MeshCore.create_serial() and retries on failure
  3. Unplug the radio (or use a device that enumerates but doesn't respond to the handshake)
  4. Watch lsof -p <pid> | grep ttyACM | wc -l climb by ~3 every retry cycle
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions