From 70d0f2b5a6ca81d94d62da3d704857b3b6b067d8 Mon Sep 17 00:00:00 2001 From: "David M. Raker" Date: Wed, 1 Oct 2025 13:57:56 -0700 Subject: [PATCH 1/5] Updates to README.md. --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7361f0f..6a2f6a3 100644 --- a/README.md +++ b/README.md @@ -1 +1,59 @@ -# lib-protocol-proxy \ No newline at end of file +# Protocol Proxy +![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg) +![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg) +[![Passing?](https://github.com/eclipse-volttron/lib-protocol-proxy/actions/workflows/run-tests.yml/badge.svg)](https://github.com/eclipse-volttron/lib-protocol-proxy/actions/workflows/run-tests.yml) +[![pypi version](https://img.shields.io/pypi/v/protocol-proxy.svg)](https://pypi.org/project/protocol-proxy/) + +This library provides the user with the ability to automatically deploy and manager proxy processes for handling +network communication with remote devices using various protocols. A proxy to each remote peer is established in +a separate process from the managing application. A manager class handles socket communication between the proxy +subprocess and its owner. Individual protocols are implemented as plugins to this library. Integration with +event and asyncio event loops are supported for each of the proxy and manager processes. + + +## Automatically installed dependencies +- python = ">=3.10,<4.0" + +[//]: # (# Documentation) + +[//]: # (More detailed documentation can be found on [ReadTheDocs](https://eclipse-volttron.readthedocs.io/en/latest/external-docs/lib-protocol-proxy/index.html. The RST source) + +[//]: # (of the documentation for this component is located in the "docs" directory of this repository.) + +# Installation +This library can be installed using pip: + +```shell +pip install lib-protocol-proxy +``` + +Protocol Proxy plugins should include "protocol-proxy" as a requirement, so users of existing +plugins are encouraged to instead install the plugin for that pacakge directly. + +# Development +This library is maintained by the VOLTTRON Development Team. + +Please see the following [guidelines](https://github.com/eclipse-volttron/volttron-core/blob/develop/CONTRIBUTING.md) +for contributing to this and/or other VOLTTRON repositories. + +[//]: # (Please see the following helpful guide about [using the Protocol Proxy](https://github.com/eclipse-volttron/lib-protocol-proxy/blob/develop/developing_with_protocol_proxy.md)) + +[//]: # (in your VOLTTRON agent or other applications.) + +# Disclaimer Notice + +This material was prepared as an account of work sponsored by an agency of the +United States Government. Neither the United States Government nor the United +States Department of Energy, nor Battelle, nor any of their employees, nor any +jurisdiction or organization that has cooperated in the development of these +materials, makes any warranty, express or implied, or assumes any legal +liability or responsibility for the accuracy, completeness, or usefulness or any +information, apparatus, product, software, or process disclosed, or represents +that its use would not infringe privately owned rights. + +Reference herein to any specific commercial product, process, or service by +trade name, trademark, manufacturer, or otherwise does not necessarily +constitute or imply its endorsement, recommendation, or favoring by the United +States Government or any agency thereof, or Battelle Memorial Institute. The +views and opinions of authors expressed herein do not necessarily state or +reflect those of the United States Government or any agency thereof. From 7487dac4cc797ede8c177ba6b99dd6ce6435e8c7 Mon Sep 17 00:00:00 2001 From: "David M. Raker" Date: Wed, 22 Oct 2025 08:29:53 -0700 Subject: [PATCH 2/5] Added try blocks around use of getpeername in ipc.gevent. --- src/protocol_proxy/ipc/gevent.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/protocol_proxy/ipc/gevent.py b/src/protocol_proxy/ipc/gevent.py index 4d8a0c1..293e987 100644 --- a/src/protocol_proxy/ipc/gevent.py +++ b/src/protocol_proxy/ipc/gevent.py @@ -146,12 +146,19 @@ def _receive_headers(self, s: socket) -> ProtocolHeaders | None: try: received = s.recv(2) if len(received) == 0: - _log.warning(f'{self.proxy_name} received closed socket from ({s.getpeername()}.') + try: + peer_name = f' from {s.getpeername()}.' + except OSError: + peer_name = '.' + _log.warning(f'{self.proxy_name} received closed socket {peer_name}') return None version_num = struct.unpack('>H', received)[0] if not (protocol := self.PROTOCOL_VERSION.get(version_num)): - raise NotImplementedError(f'Unknown protocol version ({version_num})' - f' received from: {s.getpeername()}') + try: + peer_name = f' received from: {s.getpeername()}.' + except OSError: + peer_name = '.' + raise NotImplementedError(f'Unknown protocol version ({version_num}){peer_name}') header_bytes = s.recv(protocol.HEADER_LENGTH) if len(header_bytes) == protocol.HEADER_LENGTH: return protocol.unpack(header_bytes) @@ -194,11 +201,19 @@ def _receive_socket(self, s: socket): s.close() done = True elif headers: + try: + peer_name = f' from {s.getpeername()}' + except OSError: + peer_name = '' _log.warning(f'{self.proxy_name}: Received unknown method name: {headers.method_name}' - f' from {s.getpeername()} with request ID: {headers.request_id}') + f' {peer_name} with request ID: {headers.request_id}') s.close() else: - _log.warning(f'{self.proxy_name}: Unable to read headers from socket: {s.getpeername()}') + try: + peer_name = f': {s.getpeername()}.' + except OSError: + peer_name = '.' + _log.warning(f'{self.proxy_name}: Unable to read headers from socket: {peer_name}') s.close() def _send_headers(self, s: socket, data_length: int, request_id: int, response_expected: bool, method_name: str, @@ -216,7 +231,11 @@ def _send_headers(self, s: socket, data_length: int, request_id: int, response_e def _send_socket(self, s: socket): _log.debug(f'{self.proxy_name}: IN SEND SOCKET') if not (message := self.outbound_messages.get(s)): - _log.warning(f'Outbound socket to {s.getpeername()} was ready, but no outbound message was found.') + try: + peer_name = f'to {s.getpeername()}' + except OSError: + peer_name = '' + _log.warning(f'Outbound socket to {peer_name} was ready, but no outbound message was found.') elif isinstance(message.payload, AsyncResult) and not message.payload.ready(): self.outbounds.add(s) _log.debug('IN SEND SOCKET, WAS ADDED BACK TO OUTBOUND BECAUSE ASYNC_RESULT WAS NOT READY.') From 4751e96c691d09a91bc8ce9c7e914346e8a75f1b Mon Sep 17 00:00:00 2001 From: riley206-pnnl Date: Wed, 12 Nov 2025 15:50:24 -0800 Subject: [PATCH 3/5] Refine inbound parameters extraction to handle IPv6 socket tuples correctly --- src/protocol_proxy/ipc/asyncio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/protocol_proxy/ipc/asyncio.py b/src/protocol_proxy/ipc/asyncio.py index 8945de7..8878cd4 100644 --- a/src/protocol_proxy/ipc/asyncio.py +++ b/src/protocol_proxy/ipc/asyncio.py @@ -91,7 +91,10 @@ async def _setup_inbound_server(self, socket_params: SocketParams = None): f' on any port in range: {self.min_port} - {self.max_port}.') break else: - self.inbound_params = SocketParams(*self.inbound_server.sockets[0].getsockname()) + # Only take first 2 elements (host, port) from getsockname() + # IPv6 sockets return 4-tuple (host, port, flowinfo, scope_id) + sockname = self.inbound_server.sockets[0].getsockname() + self.inbound_params = SocketParams(sockname[0], sockname[1]) break async def start(self, *_, **__): From fbf255f466451c5d9fc2198f0c66cbac35f6f7d9 Mon Sep 17 00:00:00 2001 From: riley206-pnnl Date: Wed, 12 Nov 2025 16:32:56 -0800 Subject: [PATCH 4/5] Fix formatting in pyproject.toml and update version to 2.0.0rc2 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d58ffb3..099a91c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api" profile = "black" [tool.mypy] -python_version = 3.10 +python_version = "3.10" show_error_context = true pretty = true show_column_numbers = true @@ -29,7 +29,7 @@ ignore_missing_imports = true [tool.poetry] name = "protocol-proxy" -version = "2.0.0rc0" +version = "2.0.0rc2" description = "A system for launching and communicating with a proxy application for network communication which runs in a separate process.." authors = ["The VOLTTRON Development Team "] license = "Apache License 2.0" From 148f2cb1fc267ce1b3e9b3940e16f8d88bc10805 Mon Sep 17 00:00:00 2001 From: riley206-pnnl Date: Wed, 12 Nov 2025 16:38:55 -0800 Subject: [PATCH 5/5] Refactor get_local_socket_params to handle IPv6 socket tuples correctly --- src/protocol_proxy/proxy/asyncio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/protocol_proxy/proxy/asyncio.py b/src/protocol_proxy/proxy/asyncio.py index ea15b27..f1ca147 100644 --- a/src/protocol_proxy/proxy/asyncio.py +++ b/src/protocol_proxy/proxy/asyncio.py @@ -22,7 +22,10 @@ def __init__(self, manager_address: str, manager_port: int, manager_id: UUID, ma token=manager_token) def get_local_socket_params(self) -> SocketParams: - return self.inbound_server.sockets[0].getsockname() + # Only take first 2 elements (host, port) from getsockname() + # IPv6 sockets return 4-tuple (host, port, flowinfo, scope_id) + sockname = self.inbound_server.sockets[0].getsockname() + return SocketParams(sockname[0], sockname[1]) async def send_registration(self, remote: AsyncioProtocolProxyPeer): _log.debug(f"[send_registration] Attempting to register with manager at: {remote} (type={type(remote)})")