Skip to content

Commit 1e75eb3

Browse files
authored
can, isotp, soft: threding fixes and small buffer device support improvements (#4983)
* try harder to drain before close to avoid warnings * unit tests to trigger issues encountered using soft sockt with slcan, candle, cantact on windows * threading robustness improvements * robustness improvements when using can adapters with small buffers * improvements to the heuristics for detecting can adapters with no hardware filtering
1 parent e95d0f6 commit 1e75eb3

6 files changed

Lines changed: 1678 additions & 73 deletions

File tree

.config/codespell_ignore.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ba
77
browseable
88
byteorder
99
cace
10+
cantact
1011
cas
1112
ciph
1213
componet

scapy/contrib/cansocket_python_can.py

Lines changed: 145 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from scapy.supersocket import SuperSocket
2424
from scapy.layers.can import CAN
2525
from scapy.packet import Packet
26-
from scapy.error import warning
26+
from scapy.error import warning, log_runtime
2727
from typing import (
2828
List,
2929
Type,
@@ -41,6 +41,36 @@
4141

4242
__all__ = ["CANSocket", "PythonCANSocket"]
4343

44+
# Interfaces with hardware or kernel-level CAN filtering.
45+
# These keep bus-level filters for efficiency since the device/driver
46+
# handles filtering before frames reach userspace.
47+
# All other interfaces (USB adapters like candle, gs_usb, cantact;
48+
# serial like slcan) only do software filtering in BusABC.recv() and
49+
# need filters cleared at the bus level to avoid starving matching
50+
# frames behind non-matching ones (echo frames, other bus traffic).
51+
_HW_FILTERED_INTERFACES = frozenset([
52+
'socketcan', 'kvaser', 'vector', 'pcan', 'ixxat',
53+
'nican', 'neovi', 'etas', 'systec', 'nixnet',
54+
])
55+
56+
57+
def _is_sw_filtered(interface_key):
58+
# type: (str) -> bool
59+
"""Return True if the bus identified by interface_key only does
60+
software filtering (inside BusABC.recv).
61+
62+
For such interfaces, bus-level filters must be cleared so that
63+
bus.recv(timeout=0) returns ALL frames. Per-socket filtering
64+
is then handled by distribute() via _matches_filters().
65+
66+
Without this, BusABC.recv(timeout=0) on software-filtered buses
67+
(candle, gs_usb, cantact, slcan, etc.) can silently consume
68+
one non-matching frame per call and return None, starving matching
69+
frames that sit behind it in the USB/serial buffer.
70+
"""
71+
iface = interface_key.split('_')[0].lower()
72+
return iface not in _HW_FILTERED_INTERFACES
73+
4474

4575
class SocketMapper(object):
4676
"""Internal Helper class to map a python-can bus object to
@@ -57,6 +87,14 @@ def __init__(self, bus, sockets):
5787
self.bus = bus
5888
self.sockets = sockets
5989
self.closing = False
90+
# Per-bus lock serializing read_bus() and send_bus().
91+
# On serial interfaces (slcan) the python-can Bus uses a single
92+
# serial port for both recv() and send(). Without serialization,
93+
# concurrent calls from different threads (TimeoutScheduler for
94+
# recv, main thread for send) corrupt the serial byte stream,
95+
# causing slcan parsing errors. The lock is per-mapper (per-bus)
96+
# so different CAN buses are not blocked by each other.
97+
self.bus_lock = threading.Lock()
6098

6199
# Maximum time (seconds) to spend reading frames in one read_bus()
62100
# call. On serial interfaces (slcan) the final bus.recv(timeout=0)
@@ -76,28 +114,43 @@ def read_bus(self):
76114
slcan serial timeout). This method limits total time spent
77115
reading so the TimeoutScheduler thread stays responsive.
78116
79-
This method intentionally does NOT hold pool_mutex so that
80-
concurrent send() calls are not blocked during the serial I/O.
117+
This method does NOT hold pool_mutex but DOES hold bus_lock to
118+
serialize with send_bus(). This prevents concurrent serial I/O
119+
on slcan interfaces while still allowing sends to other buses.
81120
"""
82121
if self.closing:
83122
return []
84123
msgs = []
85124
deadline = time.monotonic() + self.READ_BUS_TIME_LIMIT
86-
while True:
87-
try:
88-
msg = self.bus.recv(timeout=0)
89-
if msg is None:
125+
with self.bus_lock:
126+
while True:
127+
try:
128+
msg = self.bus.recv(timeout=0)
129+
if msg is None:
130+
break
131+
else:
132+
msgs.append(msg)
133+
if time.monotonic() >= deadline:
134+
break
135+
except Exception as e:
136+
if not self.closing:
137+
warning("[MUX] python-can exception caught: %s" % e)
90138
break
91-
else:
92-
msgs.append(msg)
93-
if time.monotonic() >= deadline:
94-
break
95-
except Exception as e:
96-
if not self.closing:
97-
warning("[MUX] python-can exception caught: %s" % e)
98-
break
99139
return msgs
100140

141+
def send_bus(self, msg):
142+
# type: (can_Message) -> None
143+
"""Send a CAN message on the bus.
144+
145+
Serialized with read_bus() via bus_lock to prevent concurrent
146+
serial I/O on slcan interfaces.
147+
148+
:param msg: python-can Message to send.
149+
:raises can_CanError: If the underlying python-can Bus raises.
150+
"""
151+
with self.bus_lock:
152+
self.bus.send(msg)
153+
101154
def distribute(self, msgs):
102155
# type: (List[can_Message]) -> None
103156
"""Distribute received messages to all subscribed sockets."""
@@ -122,10 +175,10 @@ def internal_send(self, sender, msg):
122175
123176
A given SocketWrapper wants to send a CAN message. The python-can
124177
Bus object is obtained from an internal pool of SocketMapper objects.
125-
The given message is sent on the python-can Bus object and also
126-
inserted into the message queues of all other SocketWrapper objects
127-
which are connected to the same python-can bus object
128-
by the SocketMapper.
178+
The message is sent on the python-can Bus object via send_bus()
179+
(serialized with read_bus() by bus_lock) and also inserted into
180+
the message queues of all other SocketWrapper objects connected to
181+
the same python-can bus object by the SocketMapper.
129182
130183
:param sender: SocketWrapper which initiated a send of a CAN message
131184
:param msg: CAN message to be sent
@@ -136,30 +189,43 @@ def internal_send(self, sender, msg):
136189
with self.pool_mutex:
137190
try:
138191
mapper = self.pool[sender.name]
139-
mapper.bus.send(msg)
140-
for sock in mapper.sockets:
141-
if sock == sender:
142-
continue
143-
if not sock._matches_filters(msg):
144-
continue
145-
146-
with sock.lock:
147-
sock.rx_queue.append(msg)
148192
except KeyError:
149193
warning("[SND] Socket %s not found in pool" % sender.name)
150-
except can_CanError as e:
151-
warning("[SND] python-can exception caught: %s" % e)
194+
return
195+
# Snapshot the peer sockets while under pool_mutex
196+
peers = [s for s in mapper.sockets if s != sender]
197+
198+
# Send on the bus outside pool_mutex but inside bus_lock.
199+
# This serializes with read_bus() to prevent concurrent serial
200+
# I/O on slcan interfaces, while still allowing multiplexing
201+
# and sends on other buses to proceed concurrently.
202+
try:
203+
mapper.send_bus(msg)
204+
except can_CanError as e:
205+
warning("[SND] python-can exception caught: %s" % e)
206+
return
207+
208+
# Distribute to peer sockets (no need for pool_mutex here,
209+
# we already have a snapshot of the peers list).
210+
for sock in peers:
211+
if not sock._matches_filters(msg):
212+
continue
213+
with sock.lock:
214+
sock.rx_queue.append(msg)
152215

153216
def multiplex_rx_packets(self):
154217
# type: () -> None
155218
"""This calls the mux() function of all SocketMapper
156219
objects in this SocketPool
157220
"""
158-
if time.monotonic() - self.last_call < 0.001:
221+
now = time.monotonic()
222+
if now - self.last_call < 0.001:
159223
# Avoid starvation if multiple threads are doing selects, since
160224
# this object is singleton and all python-CAN sockets are using
161225
# the same instance and locking the same locks.
162226
return
227+
self.last_call = now
228+
163229
# Snapshot pool entries under the lock, then read from each bus
164230
# WITHOUT holding pool_mutex. On slow serial interfaces (slcan)
165231
# bus.recv(timeout=0) can take ~2-3ms per frame; holding the
@@ -171,7 +237,6 @@ def multiplex_rx_packets(self):
171237
msgs = mapper.read_bus()
172238
if msgs:
173239
mapper.distribute(msgs)
174-
self.last_call = time.monotonic()
175240

176241
def register(self, socket, *args, **kwargs):
177242
# type: (SocketWrapper, Tuple[Any, ...], Dict[str, Any]) -> None
@@ -198,34 +263,37 @@ def register(self, socket, *args, **kwargs):
198263
t = self.pool[k]
199264
t.sockets.append(socket)
200265
# Update bus-level filters to the union of all sockets'
201-
# filters. For non-slcan interfaces (socketcan, kvaser,
202-
# vector), this enables efficient hardware/kernel
203-
# filtering. For slcan, the bus filters were already
204-
# cleared on creation, so this is a no-op (all sockets
205-
# on slcan share the unfiltered bus).
206-
if not k.lower().startswith('slcan'):
266+
# filters. For hardware-filtered interfaces (socketcan,
267+
# kvaser, vector, pcan, ixxat), this enables efficient
268+
# kernel/device filtering. For software-filtered
269+
# interfaces (slcan, candle, gs_usb, cantact), the bus
270+
# filters were already cleared on creation, so this is
271+
# a no-op (all sockets share the unfiltered bus).
272+
if not _is_sw_filtered(k):
207273
filters = [s.filters for s in t.sockets
208274
if s.filters is not None]
209275
if filters:
210276
t.bus.set_filters(reduce(add, filters))
211277
socket.name = k
212278
else:
213279
bus = can_Bus(*args, **kwargs)
214-
# Serial interfaces like slcan only do software
215-
# filtering inside BusABC.recv(): the recv loop reads
216-
# one frame, finds it doesn't match, and returns
217-
# None -- silently consuming serial bandwidth without
218-
# returning the frame to the mux. This starves the
219-
# mux on busy buses.
280+
# Software-filtered interfaces (slcan, candle, gs_usb,
281+
# cantact, etc.) only filter inside BusABC.recv(): the
282+
# recv loop reads one frame, finds it doesn't match,
283+
# and returns None -- silently consuming serial/USB
284+
# bandwidth without returning the frame to the mux.
285+
# On USB adapters with timeout=0 mapped to ~1ms reads,
286+
# this means only one non-matching frame is consumed
287+
# per poll cycle, starving matching ECU responses that
288+
# sit behind echo frames in the hardware buffer.
220289
#
221-
# For slcan, clear the filters from the bus so that
222-
# bus.recv() returns ALL frames. Per-socket filtering
223-
# in distribute() via _matches_filters() handles
224-
# delivery. Other interfaces (socketcan, kvaser,
225-
# vector, candle) perform efficient hardware/kernel
226-
# filtering and should keep their bus-level filters.
227-
if kwargs.get('can_filters') and \
228-
k.lower().startswith('slcan'):
290+
# For all software-filtered interfaces, clear the bus
291+
# filters so bus.recv() returns ALL frames. Per-socket
292+
# filtering in distribute() via _matches_filters()
293+
# handles delivery. Hardware-filtered interfaces
294+
# (socketcan, kvaser, vector, pcan, ixxat) keep their
295+
# bus-level filters for efficiency.
296+
if kwargs.get('can_filters') and _is_sw_filtered(k):
229297
bus.set_filters(None)
230298
socket.name = k
231299
self.pool[k] = SocketMapper(bus, [socket])
@@ -242,17 +310,25 @@ def unregister(self, socket):
242310
if socket.name is None:
243311
raise TypeError("SocketWrapper.name should never be None")
244312

313+
mapper_to_shutdown = None
245314
with self.pool_mutex:
246315
try:
247316
t = self.pool[socket.name]
248317
t.sockets.remove(socket)
249318
if not t.sockets:
250319
t.closing = True
251-
t.bus.shutdown()
252320
del self.pool[socket.name]
321+
mapper_to_shutdown = t
253322
except KeyError:
254323
warning("Socket %s already removed from pool" % socket.name)
255324

325+
# Shutdown the bus outside pool_mutex. Acquire bus_lock to
326+
# wait for any in-progress read_bus() or send_bus() to finish
327+
# before shutting down the underlying transport.
328+
if mapper_to_shutdown is not None:
329+
with mapper_to_shutdown.bus_lock:
330+
mapper_to_shutdown.bus.shutdown()
331+
256332

257333
SocketsPool = _SocketsPool()
258334

@@ -341,6 +417,9 @@ def recv_raw(self, x=0xffff):
341417
"""Returns a tuple containing (cls, pkt_data, time)"""
342418
msg = self.can_iface.recv()
343419

420+
if msg is None:
421+
return self.basecls, None, None
422+
344423
hdr = msg.is_extended_id << 31 | msg.is_remote_frame << 30 | \
345424
msg.is_error_frame << 29 | msg.arbitration_id
346425

@@ -382,7 +461,13 @@ def select(sockets, remain=conf.recv_poll_rate):
382461
:returns: an array of sockets that were selected and
383462
the function to be called next to get the packets (i.g. recv)
384463
"""
464+
# Move kernel-buffered CAN frames into the per-socket rx_queues
465+
# BEFORE checking which sockets are ready. The previous order
466+
# (check, then multiplex) returned a stale ready-list that did
467+
# not include sockets whose data had just been multiplexed,
468+
# causing a one-iteration delay.
385469
SocketsPool.multiplex_rx_packets()
470+
386471
ready_sockets = \
387472
[s for s in sockets if isinstance(s, PythonCANSocket) and
388473
len(s.can_iface.rx_queue)]
@@ -401,6 +486,13 @@ def close(self):
401486
"""Closes this socket"""
402487
if self.closed:
403488
return
489+
# Final poll to ensure all kernel-buffered frames are distributed
490+
# to any shared socket instances before we unregister.
491+
try:
492+
SocketsPool.multiplex_rx_packets()
493+
except Exception:
494+
log_runtime.debug("Exception during SocketsPool multiplex in close",
495+
exc_info=True)
404496
super(PythonCANSocket, self).close()
405497
self.can_iface.shutdown()
406498

0 commit comments

Comments
 (0)