44CyclicSendTasks.
55"""
66
7+ import concurrent .futures .thread
78import importlib
89import logging
9- from collections .abc import Iterable
10+ from collections .abc import Callable , Iterable
1011from typing import Any , Optional , Union , cast
1112
1213from . import util
@@ -140,6 +141,7 @@ def Bus( # noqa: N802
140141
141142def 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
0 commit comments