1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
|
"""
Peer connections held by processes of their own (`connection_processes`).
One process runs aiortc on one core and tops out at about 35 MB/s over all its
connections. Above 1, the node starts that many `transport/webrtc/worker.py`
processes for the stack below the data channel and keeps the sessions.
Nothing here is stubbed below the pool: the processes are real, the connection
is a real aiortc one over loopback, and the peer on the other side is an
`RTCPeerConnection` in the test. What a stub would hide is exactly what can go
wrong, a session reading something off its connection that the stand-in does
not carry.
"""
import asyncio
import struct
from pathlib import Path
import msgpack
import pytest
from aiortc import RTCPeerConnection, RTCSessionDescription
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_node import ops
from meshbay_node.config import load_config, max_processes, node_settings_defaults
from meshbay_node.roster import Roster
from meshbay_node.transport.webrtc.channel import _get_remote_ip, _pack
from meshbay_node.transport.webrtc.workers import WorkerPool
from meshbay_node.transport.webrtc_server import WebRTCTransport
async def _until(test, seconds: float = 15.0) -> None:
for _ in range(int(seconds / 0.02)):
if test():
return
await asyncio.sleep(0.02)
raise AssertionError("did not happen in time")
class _Browser:
"""The other side: a peer connection with one data channel, as a browser
opens it."""
def __init__(self) -> None:
self.pc = RTCPeerConnection()
self.channel = self.pc.createDataChannel("mnp")
self.got: list[bytes] = []
self.channel.on("message", self.got.append)
async def offer(self) -> str:
await self.pc.setLocalDescription(await self.pc.createOffer())
return self.pc.localDescription.sdp
async def accept(self, answer_sdp: str) -> None:
await self.pc.setRemoteDescription(
RTCSessionDescription(sdp=answer_sdp, type="answer"))
await _until(lambda: self.channel.readyState == "open")
async def _connect(pool: WorkerPool):
browser = _Browser()
peer = await pool.open(await browser.offer())
channels = []
peer.on("datachannel", channels.append)
await browser.accept(await peer.negotiate())
await _until(lambda: channels)
return browser, peer, channels[0]
@pytest.fixture
async def pool():
p = WorkerPool(2)
await p.ensure()
yield p
await p.close()
@pytest.mark.asyncio
async def test_bytes_cross_in_both_directions(pool):
browser, peer, channel = await _connect(pool)
try:
heard = []
channel.on("message", heard.append)
browser.channel.send(b"from the browser")
await _until(lambda: heard)
assert heard == [b"from the browser"]
channel.send(b"from the node")
await _until(lambda: browser.got)
assert browser.got == [b"from the node"]
assert channel.label == "mnp"
finally:
await browser.pc.close()
@pytest.mark.asyncio
async def test_what_is_buffered_comes_back_to_zero(pool):
"""The download path waits while `bufferedAmount` is above 2 MB (files.py).
A count that never came down would stop every transfer after its first two
chunks; one that never went up would hand the whole file to the socket."""
browser, peer, channel = await _connect(pool)
try:
chunk = bytes(60_000)
for _ in range(40):
channel.send(chunk)
assert channel.bufferedAmount == 40 * len(chunk), (
"nothing has been confirmed yet, so all of it is still buffered")
await _until(lambda: sum(map(len, browser.got)) == 40 * len(chunk))
await _until(lambda: channel.bufferedAmount == 0)
finally:
await browser.pc.close()
@pytest.mark.asyncio
async def test_the_session_reads_what_it_needs_from_the_stand_in(pool):
browser, peer, channel = await _connect(pool)
try:
await _until(lambda: peer.connectionState == "connected")
await _until(lambda: _get_remote_ip(peer))
assert peer.localDescription.sdp.count("a=fingerprint:") >= 1
assert peer.remoteDescription.sdp == browser.pc.localDescription.sdp
# The SCTP summary of the transfer log line arrives once a second.
await _until(lambda: "cwnd=" in peer.sctp_line, seconds=5)
finally:
await browser.pc.close()
@pytest.mark.asyncio
async def test_closing_says_so_once_and_frees_the_process(pool):
browser, peer, channel = await _connect(pool)
states = []
peer.on("connectionstatechange", lambda: states.append(peer.connectionState))
assert sorted(pool.loads()) == [0, 1]
await peer.close()
assert states == ["closed"]
assert channel.readyState == "closed"
assert pool.loads() == [0, 0]
await asyncio.sleep(0.3) # the process confirms; that is not a second event
assert states == ["closed"]
await browser.pc.close()
@pytest.mark.asyncio
async def test_a_process_that_dies_takes_only_its_own_connections(pool):
first = await _connect(pool)
second = await _connect(pool)
doomed = first[1]._worker
assert second[1]._worker is not doomed, "connections go to the least loaded process"
states = []
first[1].on("connectionstatechange", lambda: states.append(first[1].connectionState))
doomed.process.kill()
await _until(lambda: states == ["closed"])
assert second[1].connectionState != "closed"
# And the node is back to its number of processes for the next connection.
third = await _connect(pool)
assert pool.processes() == 2
for browser, _, _ in (first, second, third):
await browser.pc.close()
@pytest.mark.asyncio
async def test_fewer_processes_waits_for_the_connections_to_end(pool):
one = await _connect(pool)
two = await _connect(pool)
await pool.resize(1)
assert not pool.enabled
assert await pool.open("ignored") is None, (
"at one process the node holds new connections itself")
await asyncio.sleep(0.2)
assert pool.processes() == 2, "an open connection is never cut by a setting"
assert one[1].connectionState != "closed"
await one[1].close()
await two[1].close()
await _until(lambda: pool.processes() == 0)
for browser, _, _ in (one, two):
await browser.pc.close()
@pytest.mark.asyncio
async def test_only_a_process_the_node_started_is_listened_to(pool):
port = pool._server.sockets[0].getsockname()[1]
reader, writer = await asyncio.open_connection("127.0.0.1", port)
writer.write(bytes(32))
assert await reader.read() == b"", "the socket is closed on a token nobody was given"
assert pool.processes() == 2
writer.close()
@pytest.mark.asyncio
async def test_a_session_answers_through_a_connection_process():
"""The whole path: the transport gives the connection to a process, builds
the session on the stand-in, and the session's reply reaches the peer."""
transport = WebRTCTransport(
Ed25519PrivateKey.generate(), b"", b"", None, None,
stun_servers=[], connection_processes=2)
browser = _Browser()
try:
answer, _ = await transport.handle_offer(await browser.offer(), "peer-1")
await browser.accept(answer)
assert sorted(transport._workers.loads()) == [0, 1]
browser.channel.send(_pack({"type": "ping", "token": 7}))
await _until(lambda: browser.got)
(length,) = struct.unpack(">I", browser.got[0][:4])
reply = msgpack.unpackb(browser.got[0][4:4 + length], raw=False)
assert reply == {"type": "error", "detail": "Handshake required"}
await transport.close_peer("peer-1")
assert transport._workers.loads() == [0, 0]
finally:
await browser.pc.close()
await transport.close_all()
assert transport._workers.processes() == 0
@pytest.mark.asyncio
async def test_one_process_starts_none():
transport = WebRTCTransport(
Ed25519PrivateKey.generate(), b"", b"", None, None, stun_servers=[])
await transport._workers.ensure()
assert transport._workers.processes() == 0
assert not transport._workers.enabled
await transport.close_all()
# ── the setting ─────────────────────────────────────────────────────────────
def test_the_default_is_one_process(tmp_path):
assert node_settings_defaults()["connection_processes"] == 1
conf = tmp_path / "node.toml"
conf.write_text('[hub]\nurl = "https://example.invalid"\nusername = "op"\n')
assert load_config(conf).node.connection_processes == 1
def test_node_toml_cannot_ask_for_more_than_the_cores(tmp_path):
conf = tmp_path / "node.toml"
conf.write_text('[hub]\nurl = "https://example.invalid"\nusername = "op"\n\n'
'[node]\nconnection_processes = 4096\n')
assert load_config(conf).node.connection_processes == max_processes()
class _Webrtc:
def __init__(self) -> None:
self.asked: list[int] = []
async def set_connection_processes(self, n: int) -> int:
self.asked.append(n)
return n
@pytest.fixture
async def state(tmp_path: Path):
conf = tmp_path / "node.toml"
conf.write_text('[hub]\nurl = "https://example.invalid"\nusername = "op"\n\n'
'[node]\nui_port = 18000\n', encoding="utf-8")
roster = Roster(db_path=tmp_path / "roster.db")
await roster.open()
yield {"config": load_config(conf), "config_path": str(conf),
"roster": roster, "webrtc": _Webrtc(), "node_user_id": "op"}
await roster.close()
@pytest.mark.asyncio
async def test_the_setting_is_applied_now_and_kept(state, monkeypatch):
monkeypatch.setattr("meshbay_node.ops.settings.max_processes", lambda: 4)
out = await ops.set_node_settings(state, {"connection_processes": 3})
assert out == {"updated": {"connection_processes": 3}}
assert state["webrtc"].asked == [3], "written and acknowledged is not applied"
assert "connection_processes = 3" in Path(state["config_path"]).read_text()
settings = await ops.get_node_settings(state)
assert settings["connection_processes"] == 3
assert settings["cpu_count"] == 4
@pytest.mark.asyncio
async def test_the_node_page_is_told_how_many_cores_there_are(state, monkeypatch):
"""The page takes its settings from `list_groups`. It was told the ceiling
only by `get_node_settings`, which it never calls, and showed "of 1"."""
monkeypatch.setattr("meshbay_node.ops.settings.max_processes", lambda: 4)
state["groups_ctx"] = {}
page = (await ops.list_groups(state))["settings"]
assert page["cpu_count"] == 4
assert page == await ops.get_node_settings(state)
@pytest.mark.asyncio
@pytest.mark.parametrize("value", [0, -1, 5, "many", True])
async def test_a_number_the_machine_cannot_honour_is_refused(state, monkeypatch, value):
monkeypatch.setattr("meshbay_node.ops.settings.max_processes", lambda: 4)
with pytest.raises(ops.OpError):
await ops.set_node_settings(state, {"connection_processes": value})
assert state["webrtc"].asked == []
assert (await ops.get_node_settings(state))["connection_processes"] == 1
|