4
4
CyclicSendTasks.
5
5
"""
6
6
7
+ import concurrent .futures .thread
7
8
import importlib
8
9
import logging
9
- from collections .abc import Iterable
10
+ from collections .abc import Callable , Iterable
10
11
from typing import Any , Optional , Union , cast
11
12
12
13
from . import util
@@ -140,6 +141,7 @@ def Bus( # noqa: N802
140
141
141
142
def detect_available_configs (
142
143
interfaces : Union [None , str , Iterable [str ]] = None ,
144
+ timeout : float = 5.0 ,
143
145
) -> list [AutoDetectedConfig ]:
144
146
"""Detect all configurations/channels that the interfaces could
145
147
currently connect with.
@@ -148,59 +150,84 @@ def detect_available_configs(
148
150
149
151
Automated configuration detection may not be implemented by
150
152
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
152
154
for that interface.
153
155
154
156
:param interfaces: either
155
157
- the name of an interface to be searched in as a string,
156
158
- an iterable of interface names to search in, or
157
159
- `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.
158
164
:rtype: list[dict]
159
165
: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.
161
168
"""
162
169
163
- # Figure out where to search
170
+ # Determine which interfaces to search
164
171
if interfaces is None :
165
172
interfaces = BACKENDS
166
173
elif isinstance (interfaces , str ):
167
174
interfaces = (interfaces ,)
168
- # else it is supposed to be an iterable of strings
175
+ # otherwise assume iterable of strings
169
176
170
- result = []
171
- for interface in interfaces :
177
+ # Collect detection callbacks
178
+ callbacks : dict [str , Callable [[], list [AutoDetectedConfig ]]] = {}
179
+ for interface_keyword in interfaces :
172
180
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
+ )
174
185
except CanInterfaceNotImplementedError :
175
186
log_autodetect .debug (
176
187
'interface "%s" cannot be loaded for detection of available configurations' ,
177
- interface ,
188
+ interface_keyword ,
178
189
)
179
- continue
180
190
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 ] = []
205
192
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 )
206
233
return result
0 commit comments