PI-PhysikInstrumente / PIPython

Python Library for using PI controllers with GCS command language
28 stars 3 forks source link

Context managers are not properly cleaned up #8

Closed aktentasche closed 6 days ago

aktentasche commented 1 year ago

Hi,

when running the below code based on https://github.com/PI-PhysikInstrumente/PIPython/blob/master/samples/advanced/connect_socket.py to talk to a E727 I get an OSError: Bad file descriptor

from pipython.pidevice.gcscommands import GCSCommands
from pipython.pidevice.gcsmessages import GCSMessages
from pipython.pidevice.interfaces.pisocket import PISocket

with PISocket(host="<redacted>") as gateway:
    messages = GCSMessages(gateway)
    gcs = GCSCommands(messages)
    foo = gcs.qIDN()

with PISocket(host="<redacted>"") as gateway: # error occurs here
    messages = GCSMessages(gateway)
    gcs = GCSCommands(messages)
    foo = gcs.qIDN()

I also tried to use

with GCSDevice(gateway=PISocket(host="<redacted>")) as pidevice:   
    foo = pidevice.qIDN()

with GCSDevice(gateway=PISocket(host="<redacted>")) as pidevice:   # error occurs here
    foo = pidevice.qIDN()

this also throws the OSError.

The error originates from line 65 in pidevice/interfaces/pisocket.py but effectively it comes from _downcast_gcscommands_if_necessary() which is some callback and in there when

if isdeviceavailable([GCSBaseCommands, ], self._gcscommands):

is called which in turn propagates to

def isgcs30_by_qcsv(gcsmessages):
    """
    Checks if the connected controller is a GCS30 controller using the 'CSV?' command
    @param gcsmessages : pipython.pidevice.gcsmessages.GCSMessages
    @return: 'True' if the connected controller is a GCS30 controller, else 'False'
    """
    csv = gcsmessages.read('CSV?') # error occurs here
    if float(csv) <= 2.0:
        return False
    return True

Can you assist?

aktentasche commented 1 year ago

I decided to implement my own connection class because the codebase seems to be very coupled so a clean implementation made more sense to me.,

If anyone faces the same issue, here is the code:

# windows 1252 encoding
_PI_CONTROLLER_CODEPAGE = "cp1252"

class PiPiezoConnection:
    # based on pipython/pidevice/interfaces/pisocket.py
    def connect(self, hostname: str, port: int = 50000):
        logger.info(f"Connecting to {hostname=} at {port=}")
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._socket.connect((hostname, port))
        self._socket.setblocking(False)
        self._socket.setsockopt(
            socket.SOL_TCP, socket.TCP_NODELAY, 1
        )  # disable Nagle algorithm

        self._flush_input_buffer()

    def disconnect(self):
        logger.info("Disconnecting..")
        self._socket.shutdown(socket.SHUT_RDWR)
        self._socket.close()

    def send_raw_message(self, msg: str):
        msg_encoded = msg.encode(_PI_CONTROLLER_CODEPAGE) + "\n".encode(
            _PI_CONTROLLER_CODEPAGE
        )
        logger.debug(f"Sending message {msg_encoded}")
        sent_length = self._socket.send(msg_encoded)
        # need to add one because we append \n
        if sent_length != len(msg_encoded):
            logger.error(f"send_raw_message error for msg: {msg_encoded=}")

    def read_raw_message(self, timeout_s: int = 5) -> str:
        # note that it is not exactly 5 seconds. Use AcurateSleeper for that.
        current_counter_time_s = 0
        counter_fraction_s = 0.01
        while current_counter_time_s < timeout_s:
            try:
                received = self._socket.recv(2048)
                logger.debug(f"Got raw_message after ~{current_counter_time_s} seconds")
                return received.decode(
                    encoding=_PI_CONTROLLER_CODEPAGE, errors="ignore"
                )
            # if this exception occurs the input buffer is empty
            # keep trying until we hit timeout_s...
            except IOError:
                pass
            finally:
                current_counter_time_s = current_counter_time_s + counter_fraction_s
                time.sleep(counter_fraction_s)

        logger.error(
            f"Did not get any messages within {current_counter_time_s} seconds."
        )
        return "ERROR"

    def _flush_input_buffer(self):
        # ugly! i guess this is how sockets work...they throw an exception if there is
        # nothing to receive
        while True:
            try:
                self._socket.recv(2048)
            except IOError:
                break
Software-PhysikInstrumente commented 1 year ago

We can confirm this is a bug concerning the cleanup procedure of the connection status callbacks. We are working on this and will let you know as soon as an updated release is available.

PadS25 commented 1 week ago

The bug was solved with version 2.10.1.1 and is no longer present with the current versions.