#!/usr/bin/env python3
"""End to end: a Python client on one machine reaches a Python client on another.
blender-bridge/docs/P2P_BRIDGE_SPEC.md §4. This is the whole path:
python --UDS--> session-daemon ==swarm==> session-daemon --UDS--> python
which is exactly what a Blender addon will do. Nothing above the daemon is
native, and nothing in Blender is: the addon is a plain socket client, the way
multi-user 0.8.2 ended up after extension.blender.org pushed it off binary wheels.
⛔ WHAT THIS PROVES. That a frame authored in Python arrives, byte-identical, in
another Python process that shares only a topic. It proves NOTHING about grab
arbitration — that is blender-bridge/session/grab.py, gated there, and the daemon
never parses a frame.
Run:
node ../integration/layer2_testnet.js & # writes /tmp/layer_bootstrap.txt
../test/build_tests.sh && ./build.sh
./test_daemon_e2e.py
"""
import os
import socket
import struct
import subprocess
import sys
import tempfile
import time
HERE = os.path.dirname(os.path.abspath(__file__))
DAEMON = os.path.join(HERE, 'session-daemon')
passed = 0
failed = 0
def ck(name, cond, detail=""):
global passed, failed
if cond:
passed += 1
print(" PASS: %-52s %s" % (name, detail))
else:
failed += 1
print(" FAIL: %-52s %s" % (name, detail))
def send_frame(sock, payload):
"""4-byte LE length then the payload — the boundary a byte stream lacks."""
sock.sendall(struct.pack('<I', len(payload)) + payload)
def read_frame(sock, timeout=15.0):
"""Read exactly one frame, tolerating coalescing and short reads."""
sock.settimeout(timeout)
buf = b''
while len(buf) < 4:
chunk = sock.recv(4 - len(buf))
if not chunk:
return None
buf += chunk
n = struct.unpack('<I', buf)[0]
out = b''
while len(out) < n:
chunk = sock.recv(n - len(out))
if not chunk:
return None
out += chunk
return out
def main():
if not os.path.exists(DAEMON):
print("SKIP: no session-daemon binary. Run ./build.sh first.")
return 0
bootstrap = None
if os.path.exists('/tmp/layer_bootstrap.txt'):
with open('/tmp/layer_bootstrap.txt') as f:
bootstrap = f.read().strip().split(',')[0]
if not bootstrap:
print("SKIP: no testnet. Start it first:")
print(" node ../integration/layer2_testnet.js &")
return 0
print("=" * 78)
print("Daemon end-to-end (bootstrap %s)" % bootstrap)
print("=" * 78)
tmp = tempfile.mkdtemp(prefix='session-e2e-')
topic = 'blender-bridge-e2e/%d' % os.getpid()
sock_a = os.path.join(tmp, 'a.sock')
sock_b = os.path.join(tmp, 'b.sock')
procs = []
for path in (sock_a, sock_b):
procs.append(subprocess.Popen(
[DAEMON, '--topic', topic, '--bootstrap', bootstrap, '--socket', path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL))
try:
# The socket file appears as soon as the daemon binds, before the swarm
# is up. Connecting early is fine and is what an addon will do.
deadline = time.time() + 15
while time.time() < deadline:
if os.path.exists(sock_a) and os.path.exists(sock_b):
break
time.sleep(0.1)
ck("both daemons bound their sockets",
os.path.exists(sock_a) and os.path.exists(sock_b), tmp)
ca = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
cb = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
ca.connect(sock_a)
cb.connect(sock_b)
ck("two Python clients connected", True, "one per daemon")
# The peers still have to find each other on the topic. Retry rather than
# sleep a guessed interval: the first frames are simply lost until the
# channel exists, which is honest behaviour for an ephemeral channel.
payload = bytes.fromhex('00aabbccddee')
got = None
deadline = time.time() + 45
while time.time() < deadline and got is None:
send_frame(ca, payload)
try:
got = read_frame(cb, timeout=2.0)
except socket.timeout:
got = None
ck("a frame authored in Python crossed the swarm", got is not None,
"to the other Python process")
ck("it arrived byte-identical", got == payload,
got.hex() if got else "nothing arrived")
# Reverse. An untested direction is how bug #15 blocked every incoming
# connection to a C++ server while the client path passed.
back = bytes.fromhex('01112233')
send_frame(cb, back)
got2 = None
try:
got2 = read_frame(ca, timeout=10.0)
except socket.timeout:
pass
ck("the reverse direction carries a frame too", got2 == back,
got2.hex() if got2 else "nothing came back")
# A second client on the SAME daemon must see it, or two Blenders on one
# machine would be invisible to each other.
ca2 = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
ca2.connect(sock_a)
time.sleep(0.5)
local = bytes.fromhex('0299')
send_frame(cb, local)
seen = None
try:
seen = read_frame(ca2, timeout=10.0)
except socket.timeout:
pass
ck("a second client on one daemon also receives", seen == local,
seen.hex() if seen else "nothing")
# Framing: two frames in one write must not merge into one.
f1, f2 = b'\x03aaa', b'\x04bb'
ca.sendall(struct.pack('<I', len(f1)) + f1 +
struct.pack('<I', len(f2)) + f2)
r1 = read_frame(cb, timeout=10.0)
r2 = read_frame(cb, timeout=10.0)
ck("two frames in one write stay two frames", r1 == f1 and r2 == f2,
"%s then %s" % (r1.hex() if r1 else None, r2.hex() if r2 else None))
finally:
for p in procs:
p.terminate()
for p in procs:
try:
p.wait(timeout=5)
except subprocess.TimeoutExpired:
p.kill()
print()
print("%d/%d passed" % (passed, passed + failed))
return 0 if failed == 0 else 1
if __name__ == '__main__':
sys.exit(main())