Skip to content

Commit b075a68

Browse files
authored
use ThreadPoolExecutor in detect_available_configs(), add timeout param (#1947)
Co-authored-by: zariiii9003 <zariiii9003@users.noreply.github.com>
1 parent c46492b commit b075a68

File tree

5 files changed

+67
-37
lines changed

5 files changed

+67
-37
lines changed

can/interface.py

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
CyclicSendTasks.
55
"""
66

7+
import concurrent.futures.thread
78
import importlib
89
import logging
9-
from collections.abc import Iterable
10+
from collections.abc import Callable, Iterable
1011
from typing import Any, Optional, Union, cast
1112

1213
from . import util
@@ -140,6 +141,7 @@ def Bus( # noqa: N802
140141

141142
def detect_available_configs(
142143
interfaces: Union[None, str, Iterable[str]] = None,
144+
timeout: float = 5.0,
143145
) -> list[AutoDetectedConfig]:
144146
"""Detect all configurations/channels that the interfaces could
145147
currently connect with.
@@ -148,59 +150,84 @@ def detect_available_configs(
148150
149151
Automated configuration detection may not be implemented by
150152
every interface on every platform. This method will not raise
151-
an error in that case, but with rather return an empty list
153+
an error in that case, but will rather return an empty list
152154
for that interface.
153155
154156
:param interfaces: either
155157
- the name of an interface to be searched in as a string,
156158
- an iterable of interface names to search in, or
157159
- `None` to search in all known interfaces.
160+
:param timeout: maximum number of seconds to wait for all interface
161+
detection tasks to complete. If exceeded, any pending tasks
162+
will be cancelled, a warning will be logged, and the method
163+
will return results gathered so far.
158164
:rtype: list[dict]
159165
:return: an iterable of dicts, each suitable for usage in
160-
the constructor of :class:`can.BusABC`.
166+
the constructor of :class:`can.BusABC`. Interfaces that
167+
timed out will be logged as warnings and excluded.
161168
"""
162169

163-
# Figure out where to search
170+
# Determine which interfaces to search
164171
if interfaces is None:
165172
interfaces = BACKENDS
166173
elif isinstance(interfaces, str):
167174
interfaces = (interfaces,)
168-
# else it is supposed to be an iterable of strings
175+
# otherwise assume iterable of strings
169176

170-
result = []
171-
for interface in interfaces:
177+
# Collect detection callbacks
178+
callbacks: dict[str, Callable[[], list[AutoDetectedConfig]]] = {}
179+
for interface_keyword in interfaces:
172180
try:
173-
bus_class = _get_class_for_interface(interface)
181+
bus_class = _get_class_for_interface(interface_keyword)
182+
callbacks[interface_keyword] = (
183+
bus_class._detect_available_configs # pylint: disable=protected-access
184+
)
174185
except CanInterfaceNotImplementedError:
175186
log_autodetect.debug(
176187
'interface "%s" cannot be loaded for detection of available configurations',
177-
interface,
188+
interface_keyword,
178189
)
179-
continue
180190

181-
# get available channels
182-
try:
183-
available = list(
184-
bus_class._detect_available_configs() # pylint: disable=protected-access
185-
)
186-
except NotImplementedError:
187-
log_autodetect.debug(
188-
'interface "%s" does not support detection of available configurations',
189-
interface,
190-
)
191-
else:
192-
log_autodetect.debug(
193-
'interface "%s" detected %i available configurations',
194-
interface,
195-
len(available),
196-
)
197-
198-
# add the interface name to the configs if it is not already present
199-
for config in available:
200-
if "interface" not in config:
201-
config["interface"] = interface
202-
203-
# append to result
204-
result += available
191+
result: list[AutoDetectedConfig] = []
205192

193+
# Use manual executor to allow shutdown without waiting
194+
executor = concurrent.futures.ThreadPoolExecutor()
195+
try:
196+
futures_to_keyword = {
197+
executor.submit(func): kw for kw, func in callbacks.items()
198+
}
199+
done, not_done = concurrent.futures.wait(
200+
futures_to_keyword,
201+
timeout=timeout,
202+
return_when=concurrent.futures.ALL_COMPLETED,
203+
)
204+
# Log timed-out tasks
205+
if not_done:
206+
log_autodetect.warning(
207+
"Timeout (%.2fs) reached for interfaces: %s",
208+
timeout,
209+
", ".join(sorted(futures_to_keyword[fut] for fut in not_done)),
210+
)
211+
# Process completed futures
212+
for future in done:
213+
keyword = futures_to_keyword[future]
214+
try:
215+
available = future.result()
216+
except NotImplementedError:
217+
log_autodetect.debug(
218+
'interface "%s" does not support detection of available configurations',
219+
keyword,
220+
)
221+
else:
222+
log_autodetect.debug(
223+
'interface "%s" detected %i available configurations',
224+
keyword,
225+
len(available),
226+
)
227+
for config in available:
228+
config.setdefault("interface", keyword)
229+
result.extend(available)
230+
finally:
231+
# shutdown immediately, do not wait for pending threads
232+
executor.shutdown(wait=False, cancel_futures=True)
206233
return result

can/interfaces/usb2can/serial_selector.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
log = logging.getLogger("can.usb2can")
66

77
try:
8+
import pythoncom
89
import win32com.client
910
except ImportError:
1011
log.warning(
@@ -50,6 +51,7 @@ def find_serial_devices(serial_matcher: str = "") -> list[str]:
5051
only device IDs starting with this string are returned
5152
"""
5253
serial_numbers = []
54+
pythoncom.CoInitialize()
5355
wmi = win32com.client.GetObject("winmgmts:")
5456
for usb_controller in wmi.InstancesOf("Win32_USBControllerDevice"):
5557
usb_device = wmi.Get(usb_controller.Dependent)

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
]
137137

138138
# mock windows specific attributes
139-
autodoc_mock_imports = ["win32com"]
139+
autodoc_mock_imports = ["win32com", "pythoncom"]
140140
ctypes.windll = MagicMock()
141141
ctypesutil.HRESULT = ctypes.c_long
142142

doc/interfaces/gs_usb.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Geschwister Schneider and candleLight
66
Windows/Linux/Mac CAN driver based on usbfs or WinUSB WCID for Geschwister Schneider USB/CAN devices
77
and candleLight USB CAN interfaces.
88

9-
Install: ``pip install "python-can[gs_usb]"``
9+
Install: ``pip install "python-can[gs-usb]"``
1010

1111
Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection:
1212

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ docs = [
8686
"furo",
8787
]
8888
lint = [
89-
"pylint==3.2.*",
89+
"pylint==3.3.*",
9090
"ruff==0.11.12",
9191
"black==25.1.*",
9292
"mypy==1.16.*",
@@ -205,6 +205,7 @@ disable = [
205205
"too-many-branches",
206206
"too-many-instance-attributes",
207207
"too-many-locals",
208+
"too-many-positional-arguments",
208209
"too-many-public-methods",
209210
"too-many-statements",
210211
]

0 commit comments

Comments
 (0)