Skip to content

Commit 67ca1d7

Browse files
authored
todo: handled ls command in multiselect.py (#622)
1 parent 5496b27 commit 67ca1d7

File tree

5 files changed

+95
-2
lines changed

5 files changed

+95
-2
lines changed

libp2p/host/basic_host.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,29 @@ async def new_stream(
195195
net_stream.set_protocol(selected_protocol)
196196
return net_stream
197197

198+
async def send_command(self, peer_id: ID, command: str) -> list[str]:
199+
"""
200+
Send a multistream-select command to the specified peer and return
201+
the response.
202+
203+
:param peer_id: peer_id that host is connecting
204+
:param command: supported multistream-select command (e.g., "ls)
205+
:raise StreamFailure: If the stream cannot be opened or negotiation fails
206+
:return: list of strings representing the response from peer.
207+
"""
208+
new_stream = await self._network.new_stream(peer_id)
209+
210+
try:
211+
response = await self.multiselect_client.query_multistream_command(
212+
MultiselectCommunicator(new_stream), command
213+
)
214+
except MultiselectClientError as error:
215+
logger.debug("fail to open a stream to peer %s, error=%s", peer_id, error)
216+
await new_stream.reset()
217+
raise StreamFailure(f"failed to open a stream to peer {peer_id}") from error
218+
219+
return response
220+
198221
async def connect(self, peer_info: PeerInfo) -> None:
199222
"""
200223
Ensure there is a connection between this host and the peer

libp2p/protocol_muxer/multiselect.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,14 @@ async def negotiate(
6060
raise MultiselectError() from error
6161

6262
if command == "ls":
63-
# TODO: handle ls command
64-
pass
63+
supported_protocols = list(self.handlers.keys())
64+
response = "\n".join(supported_protocols) + "\n"
65+
66+
try:
67+
await communicator.write(response)
68+
except MultiselectCommunicatorError as error:
69+
raise MultiselectError() from error
70+
6571
else:
6672
protocol = TProtocol(command)
6773
if protocol in self.handlers:

libp2p/protocol_muxer/multiselect_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,36 @@ async def select_one_of(
7070

7171
raise MultiselectClientError("protocols not supported")
7272

73+
async def query_multistream_command(
74+
self, communicator: IMultiselectCommunicator, command: str
75+
) -> list[str]:
76+
"""
77+
Send a multistream-select command over the given communicator and return
78+
parsed response.
79+
80+
:param communicator: communicator to use to communicate with counterparty
81+
:param command: supported multistream-select command(e.g., ls)
82+
:raise MultiselectClientError: If the communicator fails to process data.
83+
:return: list of strings representing the response from peer.
84+
"""
85+
await self.handshake(communicator)
86+
87+
if command == "ls":
88+
try:
89+
await communicator.write("ls")
90+
except MultiselectCommunicatorError as error:
91+
raise MultiselectClientError() from error
92+
else:
93+
raise ValueError("Command not supported")
94+
95+
try:
96+
response = await communicator.read()
97+
response_list = response.strip().splitlines()
98+
except MultiselectCommunicatorError as error:
99+
raise MultiselectClientError() from error
100+
101+
return response_list
102+
73103
async def try_select(
74104
self, communicator: IMultiselectCommunicator, protocol: TProtocol
75105
) -> TProtocol:

newsfragments/622.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Feature: Support for sending `ls` command over `multistream-select` to list supported protocols from remote peer.
2+
This allows inspecting which protocol handlers a peer supports at runtime.

tests/core/protocol_muxer/test_protocol_muxer.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,35 @@ async def test_multiple_protocol_fails(security_protocol):
116116
await perform_simple_test(
117117
"", protocols_for_client, protocols_for_listener, security_protocol
118118
)
119+
120+
121+
@pytest.mark.trio
122+
async def test_multistream_command(security_protocol):
123+
supported_protocols = [PROTOCOL_ECHO, PROTOCOL_FOO, PROTOCOL_POTATO, PROTOCOL_ROCK]
124+
125+
async with HostFactory.create_batch_and_listen(
126+
2, security_protocol=security_protocol
127+
) as hosts:
128+
listener, dialer = hosts[1], hosts[0]
129+
130+
for protocol in supported_protocols:
131+
listener.set_stream_handler(
132+
protocol, create_echo_stream_handler(ACK_PREFIX)
133+
)
134+
135+
# Ensure dialer knows how to reach the listener
136+
dialer.get_peerstore().add_addrs(listener.get_id(), listener.get_addrs(), 10)
137+
138+
# Dialer asks peer to list the supported protocols using `ls`
139+
response = await dialer.send_command(listener.get_id(), "ls")
140+
141+
# We expect all supported protocols to show up
142+
for protocol in supported_protocols:
143+
assert protocol in response
144+
145+
assert "/does/not/exist" not in response
146+
assert "/foo/bar/1.2.3" not in response
147+
148+
# Dialer asks for unspoorted command
149+
with pytest.raises(ValueError, match="Command not supported"):
150+
await dialer.send_command(listener.get_id(), "random")

0 commit comments

Comments
 (0)