openrvdas.logger.readers.socket_reader

No module-level documentation available.
  1#!/usr/bin/env python3
  2import os
  3import socket
  4import tempfile
  5import atexit
  6import threading
  7from typing import Optional, Dict
  8
  9# Add parent directory to path
 10from logger.readers.reader import Reader  # noqa: E402
 11
 12# Global reference counter for channels
 13_channel_refs: Dict[str, int] = {}
 14_lock = threading.Lock()
 15
 16
 17class SocketReader(Reader):
 18    """Reader class for socket-based IPC mechanism.
 19
 20    Reads records from a Unix domain socket.
 21    """
 22
 23    def __init__(self, channel: str, timeout: Optional[float] = None,
 24                 buffer_size: int = 4096, keep_binary: bool = False, **kwargs):
 25        """Initialize a Reader for the specified channel.
 26
 27        Args:
 28            channel: A string identifier for the communication channel
 29
 30            timeout: Maximum time to wait for a record when reading (None means wait forever)
 31
 32            buffer_size: Max record size to expect
 33
 34            keep_binary: If true, don't convert received record to string
 35        """
 36        super().__init__(**kwargs)
 37
 38        self.timeout = timeout
 39        self.buffer_size = buffer_size
 40        self.keep_binary = keep_binary
 41
 42        # Create a unique socket path based on the channel name
 43        import hashlib
 44        channel_hash = hashlib.md5(channel.encode()).hexdigest()[:8]
 45        self.channel = channel
 46
 47        # Set socket path in temp directory to avoid permission issues
 48        temp_dir = tempfile.gettempdir()
 49        self.socket_path = os.path.join(temp_dir, f'ipc_socket_{channel_hash}')
 50
 51        # Socket for receiving data
 52        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
 53
 54        try:
 55            # Try to bind to the socket path
 56            self.sock.bind(self.socket_path)
 57        except OSError:
 58            # If socket already exists but no process is bound to it
 59            if os.path.exists(self.socket_path):
 60                os.unlink(self.socket_path)
 61                self.sock.bind(self.socket_path)
 62            else:
 63                raise
 64
 65        # Set socket timeout
 66        if self.timeout is not None:
 67            self.sock.settimeout(self.timeout)
 68
 69        # Update reference counter
 70        with _lock:
 71            _channel_refs[self.channel] = _channel_refs.get(self.channel, 0) + 1
 72
 73        # Register cleanup on exit
 74        atexit.register(self.close)
 75
 76    def read(self) -> str:
 77        """Read a record from the channel, blocking until one is available.
 78
 79        Returns:
 80            str: The data that was read
 81
 82        Raises:
 83            TimeoutError: If no data is available within the timeout period
 84        """
 85        try:
 86            record, _ = self.sock.recvfrom(self.buffer_size)
 87            if not self.keep_binary:
 88                record = record.decode('utf-8')
 89            return record
 90        except socket.timeout:
 91            raise TimeoutError('No data available within timeout period')
 92
 93    def close(self):
 94        """Clean up resources and potentially clean up the channel."""
 95        if not hasattr(self, 'sock') or self.sock is None:
 96            # Already closed
 97            return
 98
 99        self.sock.close()
100        self.sock = None
101
102        # Decrement reference counter and cleanup if this is the last reference
103        with _lock:
104            _channel_refs[self.channel] = _channel_refs.get(self.channel, 1) - 1
105            if _channel_refs[self.channel] <= 0:
106                if os.path.exists(self.socket_path):
107                    os.unlink(self.socket_path)
108
109                # Remove from reference counter
110                _channel_refs.pop(self.channel, None)
111
112    def __del__(self):
113        """Ensure cleanup happens."""
114        self.close()
class SocketReader(logger.readers.reader.Reader):
 18class SocketReader(Reader):
 19    """Reader class for socket-based IPC mechanism.
 20
 21    Reads records from a Unix domain socket.
 22    """
 23
 24    def __init__(self, channel: str, timeout: Optional[float] = None,
 25                 buffer_size: int = 4096, keep_binary: bool = False, **kwargs):
 26        """Initialize a Reader for the specified channel.
 27
 28        Args:
 29            channel: A string identifier for the communication channel
 30
 31            timeout: Maximum time to wait for a record when reading (None means wait forever)
 32
 33            buffer_size: Max record size to expect
 34
 35            keep_binary: If true, don't convert received record to string
 36        """
 37        super().__init__(**kwargs)
 38
 39        self.timeout = timeout
 40        self.buffer_size = buffer_size
 41        self.keep_binary = keep_binary
 42
 43        # Create a unique socket path based on the channel name
 44        import hashlib
 45        channel_hash = hashlib.md5(channel.encode()).hexdigest()[:8]
 46        self.channel = channel
 47
 48        # Set socket path in temp directory to avoid permission issues
 49        temp_dir = tempfile.gettempdir()
 50        self.socket_path = os.path.join(temp_dir, f'ipc_socket_{channel_hash}')
 51
 52        # Socket for receiving data
 53        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
 54
 55        try:
 56            # Try to bind to the socket path
 57            self.sock.bind(self.socket_path)
 58        except OSError:
 59            # If socket already exists but no process is bound to it
 60            if os.path.exists(self.socket_path):
 61                os.unlink(self.socket_path)
 62                self.sock.bind(self.socket_path)
 63            else:
 64                raise
 65
 66        # Set socket timeout
 67        if self.timeout is not None:
 68            self.sock.settimeout(self.timeout)
 69
 70        # Update reference counter
 71        with _lock:
 72            _channel_refs[self.channel] = _channel_refs.get(self.channel, 0) + 1
 73
 74        # Register cleanup on exit
 75        atexit.register(self.close)
 76
 77    def read(self) -> str:
 78        """Read a record from the channel, blocking until one is available.
 79
 80        Returns:
 81            str: The data that was read
 82
 83        Raises:
 84            TimeoutError: If no data is available within the timeout period
 85        """
 86        try:
 87            record, _ = self.sock.recvfrom(self.buffer_size)
 88            if not self.keep_binary:
 89                record = record.decode('utf-8')
 90            return record
 91        except socket.timeout:
 92            raise TimeoutError('No data available within timeout period')
 93
 94    def close(self):
 95        """Clean up resources and potentially clean up the channel."""
 96        if not hasattr(self, 'sock') or self.sock is None:
 97            # Already closed
 98            return
 99
100        self.sock.close()
101        self.sock = None
102
103        # Decrement reference counter and cleanup if this is the last reference
104        with _lock:
105            _channel_refs[self.channel] = _channel_refs.get(self.channel, 1) - 1
106            if _channel_refs[self.channel] <= 0:
107                if os.path.exists(self.socket_path):
108                    os.unlink(self.socket_path)
109
110                # Remove from reference counter
111                _channel_refs.pop(self.channel, None)
112
113    def __del__(self):
114        """Ensure cleanup happens."""
115        self.close()

Reader class for socket-based IPC mechanism.

Reads records from a Unix domain socket.

SocketReader( channel: str, timeout: Optional[float] = None, buffer_size: int = 4096, keep_binary: bool = False, **kwargs)
24    def __init__(self, channel: str, timeout: Optional[float] = None,
25                 buffer_size: int = 4096, keep_binary: bool = False, **kwargs):
26        """Initialize a Reader for the specified channel.
27
28        Args:
29            channel: A string identifier for the communication channel
30
31            timeout: Maximum time to wait for a record when reading (None means wait forever)
32
33            buffer_size: Max record size to expect
34
35            keep_binary: If true, don't convert received record to string
36        """
37        super().__init__(**kwargs)
38
39        self.timeout = timeout
40        self.buffer_size = buffer_size
41        self.keep_binary = keep_binary
42
43        # Create a unique socket path based on the channel name
44        import hashlib
45        channel_hash = hashlib.md5(channel.encode()).hexdigest()[:8]
46        self.channel = channel
47
48        # Set socket path in temp directory to avoid permission issues
49        temp_dir = tempfile.gettempdir()
50        self.socket_path = os.path.join(temp_dir, f'ipc_socket_{channel_hash}')
51
52        # Socket for receiving data
53        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
54
55        try:
56            # Try to bind to the socket path
57            self.sock.bind(self.socket_path)
58        except OSError:
59            # If socket already exists but no process is bound to it
60            if os.path.exists(self.socket_path):
61                os.unlink(self.socket_path)
62                self.sock.bind(self.socket_path)
63            else:
64                raise
65
66        # Set socket timeout
67        if self.timeout is not None:
68            self.sock.settimeout(self.timeout)
69
70        # Update reference counter
71        with _lock:
72            _channel_refs[self.channel] = _channel_refs.get(self.channel, 0) + 1
73
74        # Register cleanup on exit
75        atexit.register(self.close)

Initialize a Reader for the specified channel.

Args: channel: A string identifier for the communication channel

timeout: Maximum time to wait for a record when reading (None means wait forever)

buffer_size: Max record size to expect

keep_binary: If true, don't convert received record to string
timeout
buffer_size
keep_binary
channel
socket_path
sock
def read(self) -> str:
77    def read(self) -> str:
78        """Read a record from the channel, blocking until one is available.
79
80        Returns:
81            str: The data that was read
82
83        Raises:
84            TimeoutError: If no data is available within the timeout period
85        """
86        try:
87            record, _ = self.sock.recvfrom(self.buffer_size)
88            if not self.keep_binary:
89                record = record.decode('utf-8')
90            return record
91        except socket.timeout:
92            raise TimeoutError('No data available within timeout period')

Read a record from the channel, blocking until one is available.

Returns: str: The data that was read

Raises: TimeoutError: If no data is available within the timeout period

def close(self):
 94    def close(self):
 95        """Clean up resources and potentially clean up the channel."""
 96        if not hasattr(self, 'sock') or self.sock is None:
 97            # Already closed
 98            return
 99
100        self.sock.close()
101        self.sock = None
102
103        # Decrement reference counter and cleanup if this is the last reference
104        with _lock:
105            _channel_refs[self.channel] = _channel_refs.get(self.channel, 1) - 1
106            if _channel_refs[self.channel] <= 0:
107                if os.path.exists(self.socket_path):
108                    os.unlink(self.socket_path)
109
110                # Remove from reference counter
111                _channel_refs.pop(self.channel, None)

Clean up resources and potentially clean up the channel.