From a6203f1a5a56546e3e3c4c27a95c59c153335fa6 Mon Sep 17 00:00:00 2001 From: Stef Coene Date: Tue, 19 May 2026 13:13:17 +0200 Subject: [PATCH 1/2] fix: handle missing USB string descriptors in linux_list_serial_ports USB devices are not required to provide manufacturer, product, or serial number string descriptors (indicated by index 0 in the USB device descriptor). When these descriptors are absent the corresponding sysfs files do not exist, causing read_text() to raise FileNotFoundError. Previously the entire OSError catch block skipped the device with `continue`, so any device without a serial number (a common case) was silently excluded from the port list. Fix by reading the optional fields via _read_optional_sysfs(), which returns None on OSError, while keeping mandatory fields (vid, pid, bcd_device, interface_num) in the existing try/except that skips devices that disappear during iteration. Co-Authored-By: Claude Sonnet 4.6 --- serialx/platforms/serial_linux.py | 86 +++++++++++++++++++------------ 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/serialx/platforms/serial_linux.py b/serialx/platforms/serial_linux.py index 5df26c5..9ce428c 100644 --- a/serialx/platforms/serial_linux.py +++ b/serialx/platforms/serial_linux.py @@ -180,6 +180,14 @@ def iterdir_safe(path: Path) -> Iterator[Path]: yield from path.iterdir() +def _read_optional_sysfs(path: Path) -> str | None: + """Read a sysfs string file, returning None if it does not exist.""" + try: + return path.read_text()[:-1] + except OSError: + return None + + def linux_list_serial_ports() -> list[SerialPortInfo]: """List serial ports on Linux.""" by_id_symlinks = {} @@ -217,29 +225,34 @@ def linux_list_serial_ports() -> list[SerialPortInfo]: interface_file = usb_interface / "interface" try: - info = SerialPortInfo( - device=str(unique_device), - resolved_device=str(device), - vid=int((usb_device / "idVendor").read_text(), 16), - pid=int((usb_device / "idProduct").read_text(), 16), - serial_number=(usb_device / "serial").read_text()[:-1], - manufacturer=(usb_device / "manufacturer").read_text()[:-1], - product=(usb_device / "product").read_text()[:-1], - bcd_device=int((usb_device / "bcdDevice").read_text(), 16), - interface_description=( - interface_file.read_text()[:-1] - if interface_file.exists() - else None - ), - interface_num=int( - (usb_interface / "bInterfaceNumber").read_text(), 16 - ), + vid = int((usb_device / "idVendor").read_text(), 16) + pid = int((usb_device / "idProduct").read_text(), 16) + bcd_device = int((usb_device / "bcdDevice").read_text(), 16) + interface_num = int( + (usb_interface / "bInterfaceNumber").read_text(), 16 ) except OSError: LOGGER.debug( "Serial device %r disappeared during iteration", usb_device ) continue + + info = SerialPortInfo( + device=str(unique_device), + resolved_device=str(device), + vid=vid, + pid=pid, + serial_number=_read_optional_sysfs(usb_device / "serial"), + manufacturer=_read_optional_sysfs(usb_device / "manufacturer"), + product=_read_optional_sysfs(usb_device / "product"), + bcd_device=bcd_device, + interface_description=( + interface_file.read_text()[:-1] + if interface_file.exists() + else None + ), + interface_num=interface_num, + ) elif subsystem == "usb": # CDC ACM devices usb_interface = resolved @@ -247,27 +260,32 @@ def linux_list_serial_ports() -> list[SerialPortInfo]: interface_file = usb_interface / "interface" try: - info = SerialPortInfo( - device=str(unique_device), - resolved_device=str(device), - vid=int((usb_device / "idVendor").read_text(), 16), - pid=int((usb_device / "idProduct").read_text(), 16), - serial_number=(usb_device / "serial").read_text()[:-1], - manufacturer=(usb_device / "manufacturer").read_text()[:-1], - product=(usb_device / "product").read_text()[:-1], - bcd_device=int((usb_device / "bcdDevice").read_text(), 16), - interface_description=( - interface_file.read_text()[:-1] - if interface_file.exists() - else None - ), - interface_num=int( - (usb_interface / "bInterfaceNumber").read_text(), 16 - ), + vid = int((usb_device / "idVendor").read_text(), 16) + pid = int((usb_device / "idProduct").read_text(), 16) + bcd_device = int((usb_device / "bcdDevice").read_text(), 16) + interface_num = int( + (usb_interface / "bInterfaceNumber").read_text(), 16 ) except OSError: LOGGER.debug("USB device %r disappeared during iteration", usb_device) continue + + info = SerialPortInfo( + device=str(unique_device), + resolved_device=str(device), + vid=vid, + pid=pid, + serial_number=_read_optional_sysfs(usb_device / "serial"), + manufacturer=_read_optional_sysfs(usb_device / "manufacturer"), + product=_read_optional_sysfs(usb_device / "product"), + bcd_device=bcd_device, + interface_description=( + interface_file.read_text()[:-1] + if interface_file.exists() + else None + ), + interface_num=interface_num, + ) elif subsystem in ("serial-base", "platform", "pnp", "amba"): # `serial-base` is the per-port subsystem introduced in Linux 6.10. # Older kernels expose native ports through their bus directly: From e530e3a384ddedd13c5bb3bfcf95ad3af4d7bbd3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 20 May 2026 13:49:31 -0400 Subject: [PATCH 2/2] Guard `interface_file.read_text()[:-1]` --- serialx/platforms/serial_linux.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/serialx/platforms/serial_linux.py b/serialx/platforms/serial_linux.py index 9ce428c..30fcb5c 100644 --- a/serialx/platforms/serial_linux.py +++ b/serialx/platforms/serial_linux.py @@ -222,7 +222,6 @@ def linux_list_serial_ports() -> list[SerialPortInfo]: # USB-serial chips usb_interface = resolved.parent usb_device = usb_interface.parent - interface_file = usb_interface / "interface" try: vid = int((usb_device / "idVendor").read_text(), 16) @@ -246,18 +245,13 @@ def linux_list_serial_ports() -> list[SerialPortInfo]: manufacturer=_read_optional_sysfs(usb_device / "manufacturer"), product=_read_optional_sysfs(usb_device / "product"), bcd_device=bcd_device, - interface_description=( - interface_file.read_text()[:-1] - if interface_file.exists() - else None - ), + interface_description=_read_optional_sysfs(usb_interface / "interface"), interface_num=interface_num, ) elif subsystem == "usb": # CDC ACM devices usb_interface = resolved usb_device = usb_interface.parent - interface_file = usb_interface / "interface" try: vid = int((usb_device / "idVendor").read_text(), 16) @@ -279,11 +273,7 @@ def linux_list_serial_ports() -> list[SerialPortInfo]: manufacturer=_read_optional_sysfs(usb_device / "manufacturer"), product=_read_optional_sysfs(usb_device / "product"), bcd_device=bcd_device, - interface_description=( - interface_file.read_text()[:-1] - if interface_file.exists() - else None - ), + interface_description=_read_optional_sysfs(usb_interface / "interface"), interface_num=interface_num, ) elif subsystem in ("serial-base", "platform", "pnp", "amba"):