diff --git a/serial/serialwin32.py b/serial/serialwin32.py index b8112a52..0c430ceb 100644 --- a/serial/serialwin32.py +++ b/serial/serialwin32.py @@ -13,6 +13,7 @@ # pylint: disable=invalid-name,too-few-public-methods import ctypes +import re import time from serial import win32 @@ -20,6 +21,23 @@ from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException +LEGACY_DOS_DEVICE_NAMES = frozenset('COM{}'.format(number) for number in range(1, 10)) +COM_PORT_NAME = re.compile('COM[0-9]+', re.IGNORECASE) + + +def device_path(port): + r"""Return the name that Windows resolves to the given serial port. + + Windows resolves only the bare names COM1 through COM9 as MS-DOS device + aliases. No other serial port has an alias, so COM0 as well as COM10 and + above must be named in the "\\.\COMx" device namespace format; a bare name + would be resolved as an ordinary relative file system path instead. + """ + if COM_PORT_NAME.fullmatch(port) and port.upper() not in LEGACY_DOS_DEVICE_NAMES: + return '\\\\.\\' + port + return port + + class Serial(SerialBase): """Serial port implementation for Win32 based on ctypes.""" @@ -41,18 +59,8 @@ def open(self): raise SerialException("Port must be configured before it can be used.") if self.is_open: raise SerialException("Port is already open.") - # the "\\.\COMx" format is required for devices other than COM1-COM8 - # not all versions of windows seem to support this properly - # so that the first few ports are used with the DOS device name - port = self.name - try: - if port.upper().startswith('COM') and int(port[3:]) > 8: - port = '\\\\.\\' + port - except ValueError: - # for like COMnotanumber - pass self._port_handle = win32.CreateFile( - port, + device_path(self.name), win32.GENERIC_READ | win32.GENERIC_WRITE, 0, # exclusive access None, # no security diff --git a/test/test_serialwin32.py b/test/test_serialwin32.py new file mode 100644 index 00000000..9f232018 --- /dev/null +++ b/test/test_serialwin32.py @@ -0,0 +1,66 @@ +# This file is part of pySerial - Cross platform serial port support for Python +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Test the Win32 serial port naming. +""" + +import os + +import pytest + +if os.name == "nt": + from serial import serialwin32, win32 + +pytestmark = pytest.mark.skipif(os.name != "nt", reason="Windows only") + + +@pytest.mark.parametrize( + "port, expected", + ( + ("COM1", "COM1"), + ("COM8", "COM8"), + ("COM9", "COM9"), + ("com3", "com3"), + ("COM0", r"\\.\COM0"), + ("com0", r"\\.\com0"), + ("COM00", r"\\.\COM00"), + ("COM01", r"\\.\COM01"), + ("COM10", r"\\.\COM10"), + ("COM255", r"\\.\COM255"), + (r"\\.\COM1", r"\\.\COM1"), + (r"\\.\COM0", r"\\.\COM0"), + ("COM", "COM"), + ("COMnotanumber", "COMnotanumber"), + ("/dev/ttyS0", "/dev/ttyS0"), + ), +) +def test_device_path(port, expected): + """Verify that every port without an MS-DOS device alias is prefixed.""" + + assert serialwin32.device_path(port) == expected + + +@pytest.mark.parametrize( + "port, expected", + ( + pytest.param("COM0", r"\\.\COM0", id="device-namespace"), + pytest.param("COM1", "COM1", id="ms-dos-alias"), + ), +) +def test_open_names_the_device(monkeypatch, port, expected): + """Verify that `open()` hands the resolvable name to CreateFile.""" + + names = [] + + def create_file(name, *args): + names.append(name) + return win32.INVALID_HANDLE_VALUE + + monkeypatch.setattr(win32, "CreateFile", create_file) + + with pytest.raises(serialwin32.SerialException): + serialwin32.Serial(port) + + assert names == [expected]