Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
# lib-protocol-proxy
# 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
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in "manager": "the ability to automatically deploy and manager proxy processes" should be "the ability to automatically deploy and manage proxy processes".

Suggested change
This library provides the user with the ability to automatically deploy and manager proxy processes for handling
This library provides the user with the ability to automatically deploy and manage proxy processes for handling

Copilot uses AI. Check for mistakes.
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]&#40;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.
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in "pacakge": "install the plugin for that pacakge directly" should be "install the plugin for that package directly".

Suggested change
plugins are encouraged to instead install the plugin for that pacakge directly.
plugins are encouraged to instead install the plugin for that package directly.

Copilot uses AI. Check for mistakes.

# 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]&#40;https://github.com/eclipse-volttron/lib-protocol-proxy/blob/develop/developing_with_protocol_proxy.md&#41;)

[//]: # (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.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +29,7 @@ ignore_missing_imports = true

[tool.poetry]
name = "protocol-proxy"
version = "2.0.0rc1"
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 <volttron@pnnl.gov>"]
license = "Apache License 2.0"
Expand Down
5 changes: 4 additions & 1 deletion src/protocol_proxy/ipc/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, *_, **__):
Expand Down
31 changes: 25 additions & 6 deletions src/protocol_proxy/ipc/gevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}.'
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String formatting issue: When getpeername() succeeds, peer_name is set to f' from {s.getpeername()}.' (with a trailing period), but the log message is f'{self.proxy_name} received closed socket {peer_name}' which will produce a period in the middle of the sentence when the peer name is available. The period should be removed from the peer_name assignment on line 150, or added to line 153 after {peer_name}.

Suggested change
peer_name = f' from {s.getpeername()}.'
peer_name = f' from {s.getpeername()}'

Copilot uses AI. Check for mistakes.
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()}.'
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String formatting issue: When getpeername() succeeds, peer_name is set to f' received from: {s.getpeername()}.' (with a trailing period), but this creates double periods in the error message. The period should be removed from the peer_name assignment on line 158, or the message should be structured differently to avoid duplication.

Suggested change
peer_name = f' received from: {s.getpeername()}.'
peer_name = f' received from: {s.getpeername()}'

Copilot uses AI. Check for mistakes.
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)
Expand Down Expand Up @@ -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}')
Comment on lines 208 to +209
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String formatting issue: The log message has an extra space before {peer_name}. It should be f'{self.proxy_name}: Received unknown method name: {headers.method_name}{peer_name} with request ID: {headers.request_id}' (without the space before {peer_name}). When peer_name is empty, it produces an extra space, and when it's populated with from [address], it already has a leading space.

Copilot uses AI. Check for mistakes.
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}')
Comment on lines +213 to +216
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String formatting issue: When getpeername() succeeds, peer_name is set to f': {s.getpeername()}.' (with a leading colon and trailing period), but the log message already has : before it, creating double colons: "Unable to read headers from socket: : [address].". The leading colon should be removed from the peer_name assignment on line 213.

Suggested change
peer_name = f': {s.getpeername()}.'
except OSError:
peer_name = '.'
_log.warning(f'{self.proxy_name}: Unable to read headers from socket: {peer_name}')
peer_name = f' {s.getpeername()}'
except OSError:
peer_name = ''
_log.warning(f'{self.proxy_name}: Unable to read headers from socket:{peer_name}')

Copilot uses AI. Check for mistakes.
s.close()

def _send_headers(self, s: socket, data_length: int, request_id: int, response_expected: bool, method_name: str,
Expand All @@ -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.')
Comment on lines +235 to +238
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String formatting issue: The log message is missing "socket" between "Outbound" and the peer name. It should be f'Outbound socket {peer_name} was ready, but no outbound message was found.' When peer_name is empty, it produces "Outbound socket to was ready" which is grammatically incorrect, and when it's populated, it produces "Outbound socket to to [address]" with duplicate "to".

Suggested change
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.')
peer_name = s.getpeername()
except OSError:
peer_name = None
if peer_name:
_log.warning(f'Outbound socket {peer_name} was ready, but no outbound message was found.')
else:
_log.warning('Outbound socket was ready, but no outbound message was found.')

Copilot uses AI. Check for mistakes.
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.')
Expand Down
5 changes: 4 additions & 1 deletion src/protocol_proxy/proxy/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)})")
Expand Down
Loading