Skip to content

Design issue: Using "notifier" inside "with can.Bus(…) as bus" statement has no sufficient clean-up for reader threads #1526

Open
@wolfraven

Description

@wolfraven

Description up to exception list:

There are some community posts, as well as some Python examples, suggest to create a bus object using the "with" statement. See also section "Example usage" in the python-can repository‘s "README.rst" file. The typical form:

# create a bus instance using 'with' statement,
# this will cause bus.shutdown() to be called on the block exit;
# many other interfaces are supported as well (see documentation)
with can.Bus(interface='socketcan',
              channel='vcan0',
              receive_own_messages=True) as bus:

The commend describes the reason, namely calling bus.shutdown() at block exit.

But now, using an asynchronous Notifier inside this with-block in conjunction with a serial port!
For example, having slcan as interface and the common keyboard termination construct try: … except KeyboardInterrupt: shown below:

import can
import time

try:
    # create a bus instance using 'with' statement,
    # this will cause bus.shutdown() to be called on the block exit;
    # many other interfaces are supported as well (see documentation)
    with can.Bus(interface="slcan", channel="/dev/ttyCMa01", ttyBaudrate=4000000, \
                 bitrate=500000, sleep_after_open=0.1, rtscts=False) as bus:
        #
        # Start Asynchronous Notifier with console output
        can.Notifier(bus, [can.Printer()]) # Attention: Second parameter must be a list

        # Perform a periodic message
        message_test = can.Message(arbitration_id=0x0001, is_extended_id=False, data=[])
        bus.send_periodic(message_test, period=1.0)
        
        # Sleep some time, while the periodics will be send
        time.sleep(10.0)


except KeyboardInterrupt:
    print ("KeyboardInterrupt -> shutdown() ...")

Running this as a Python script in a command line, nothing get obvious. But running the script in an IDE – Spyder in this case – an exception cascade raise up, regardless after regular termination (10 seconds example) or after a keyboard interrupt:

runfile('/home/werner/Python/pycan_test_notifier-and-send-periodic.py', wdir='/home/werner/Python')
KeyboardInterrupt -> shutdown() ...
Exception in thread can.notifier for bus "unknown":
Traceback (most recent call last):
  File "/home/werner/.local/lib/python3.8/site-packages/can/exceptions.py", line 123, in error_check
    yield
  File "/home/werner/.local/lib/python3.8/site-packages/can/interfaces/slcan.py", line 164, in _read
    in_waiting = self.serialPortOrig.in_waiting
  File "/home/werner/.local/lib/python3.8/site-packages/serial/serialposix.py", line 549, in in_waiting
    s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)
OSError: [Errno 9] Ungültiger Dateideskriptor

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.8/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/home/werner/.local/lib/python3.8/site-packages/can/notifier.py", line 122, in _rx_thread
    msg = bus.recv(self.timeout)
  File "/home/werner/.local/lib/python3.8/site-packages/can/bus.py", line 98, in recv
    msg, already_filtered = self._recv_internal(timeout=time_left)
  File "/home/werner/.local/lib/python3.8/site-packages/can/interfaces/slcan.py", line 202, in _recv_internal
    string = self._read(timeout)
  File "/home/werner/.local/lib/python3.8/site-packages/can/interfaces/slcan.py", line 180, in _read
    return None
  File "/usr/lib/python3.8/contextlib.py", line 131, in __exit__
    self.gen.throw(type, value, traceback)
  File "/home/werner/.local/lib/python3.8/site-packages/can/exceptions.py", line 128, in error_check
    raise exception_type(error_message) from error
can.exceptions.CanOperationError: Could not read from serial device

The issue:

can.Notifier get the bus object as first initialization parameter and instantiate an asynchronous reader thread (_rx_thread), listening on the serial port, which is given (and owned) by the bus object.

If now the with-block ends, the bus object will be destroyed by the clean-up process (including the bus.shutdown() call) which also destroys the contained and used serial port (from serialposix.py).
The notifier still exists at this moment and also it‘s asynchronous receiver thread – but the serial port is thrown away underneath, causing the Exception in thread can.notifier for bus "unknown".

An exception should be used as an exception – but not as part of the regular program flow. Therefore a reverse interaction between the bus and notifier objects became necessary, to realize a well ordered clean-up.

Possible solution:

To open a discussion, here a suggestion how this could be realized:

In Class can.BusABC:

  • Adding a member for the reference to a notifier:
    # Keep track about occupation by a notifier
    _notifier: Any = None
  • Adding some methods to implement administration of notifier relationship:
    @property
    def notifier(self):
        """
        Return the current notifier
        """
        return self._notifier

    def set_notifier(self, new_notifier, replace:bool=False) -> None:
        """
        Set the new notifier
        
        :raises ~can.exceptions.CanOperationError:
            If the bus still occupied by an other notifier and replace flag is False.
        """
        if self._notifier == None:
            self._notifier = new_notifier
        else:
            if replace == False:
                raise CanOperationError("Bus still occupied by a notifier.")
            else:
                self.release_from_notifier()
                self._notifier = new_notifier

    def release_from_notifier(self):
        """
        Remove this bus from current notifier
        """
        if self._notifier != None:
            # Mypy prefers this over a hasattr(...) check
            getattr(self._notifier, "remove_bus", lambda: None)(self)
  • And finally extent the shutdown method to force release from a notifier:
    def shutdown(self) -> None:
        """
        Called to carry out any interface specific cleanup required
        in shutting down a bus.
        """
        self.stop_all_periodic_tasks()
        self.release_from_notifier()

In Class can.Notifier:

  • The method add_bus is extended to inform the bus object about the notifier binding:
    def add_bus(self, bus: BusABC) -> None:
        ...
        # Inform bus about occupation by a notifier
        bus.set_notifier(self)
  • And another method becomes necessary: One to terminate a reader thread and release a bus:
    def remove_bus(self, bus: BusABC, timeout: float = 5) -> None:
        """Remove a bus from notification.

        :param bus:
            CAN bus instance.
        """
        if len(self._readers) == 1:
            self.stop() # single bus, stop listeners too 
            return

        end_time = time.time() + timeout
        for reader in self._readers:
            if reader == bus:
                if isinstance(reader, threading.Thread):
                    now = time.time()
                    if now < end_time:
                        reader.join(end_time - now)
                elif self._loop:
                    # reader is a file descriptor
                    self._loop.remove_reader(reader)

How it works:

Instantiate:

Starting to instantiate an asynchronous Notifier with:

can.Notifier(bus, [can.Printer()]) 
	-->  __init__(self, bus: ...)
		-->  self.add_bus(each_bus)
			-->  bus.set_notifier(self)

The bus variable is initialized by the with-statement and given as first parameter to the Notifiers class initialization method __init__. Inside the method add_bus is called, which create a reader (thread) for the bus (or each bus in a list) and append it to the _readers list. Now (new) the bus instance’s set_notifier method is called, with the notifiers self-reference as parameter. This way the bus get knowledge about the notifier occupation, which is internally tracked by the _notifier member variable.

Clean-up:

__exit__(self, ...):
	--> self.shutdown()
		--> self.stop_all_periodic_tasks()
		--> self.release_from_notifier() 
			--> getattr(self._notifier, "remove_bus", lambda: None)(self)

The clean-up process – e.g. on end of the with-block or bail out by an exception – is entered by the bus objects __exit__ method, which calls the shutdown method. The shutdown first clean-up all periodic tasks and (new) now also call release_from_notifier, which in turn call the related _notifier ‘s remove_bus method, which in turn terminate the running reader thread and remove it from the _readers list.

If now the bus object is destroyed, no more running tasks exists, which could throw exceptions. The notifier object of course will be destroyed just after the bus, because of it’s creation inside the with-block. But that is always clean, because running reader threads still terminated and removed before.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions