ci(e2e): on-demand Meshtasticator e2e job for the docker-enabled runner
Adds .gitea/workflows/meshtasticator-e2e.yaml (workflow_dispatch until validated green, pull_request trigger commented out): builds the pinned Hermes contract env like release.yaml, installs the plugin with deps, checks out Meshtasticator at a pinned commit, installs the simulator's own (conflicting-version) requirements in an isolated venv, then runs scripts/meshtasticator_e2e/run_e2e.sh --mode docker. Supporting changes: - run_e2e.sh: SIM_VENV support; automatic localhost->daemon TCP forwards (scripts/meshtasticator_e2e/tcp_forward.py) when DOCKER_HOST is a tcp:// daemon, because Meshtasticator hardcodes localhost for its node control connections; node-boot settle wait; fixed docker cleanup (container 'Meshtastic', volume 'Meshtasticator'). - e2e tests: connect via a module-long event loop in a background thread (adapter requires a running loop for watchdog + inbound dispatch, so per-call asyncio.run was wrong) with retry while the node TCP API boots. - README: sim-venv guidance, remote-daemon forwarding, CI section. Docker access follows the deployment examples in eric/actions-docker (docker is available to runner jobs).
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal TCP forwarder used by run_e2e.sh when the Docker daemon is remote.
|
||||
|
||||
Meshtasticator's interactive simulator always connects to its simulated nodes
|
||||
at localhost:<port> (lib/interactive.py init_communication). On a Gitea
|
||||
Actions runner whose Docker daemon is remote (e.g. DOCKER_HOST=tcp://docker.local:2375)
|
||||
the container-published node ports live on the *daemon* host, not the job's
|
||||
localhost — so we bind 127.0.0.1:<port> in the job and forward to the daemon
|
||||
host, restoring the localhost view the simulator (and our e2e tests) expect.
|
||||
|
||||
Usage: tcp_forward.py --listen 127.0.0.1:4404 --target docker.local:4404
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import threading
|
||||
|
||||
|
||||
def _forward(src: socket.socket, dst: socket.socket):
|
||||
try:
|
||||
while True:
|
||||
data = src.recv(65536)
|
||||
if not data:
|
||||
break
|
||||
dst.sendall(data)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
dst.shutdown(socket.SHUT_WR)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _handle(client: socket.socket, target_addr):
|
||||
try:
|
||||
upstream = socket.create_connection(target_addr, timeout=10)
|
||||
except OSError:
|
||||
client.close()
|
||||
return
|
||||
t = threading.Thread(target=_forward, args=(client, upstream), daemon=True)
|
||||
t.start()
|
||||
_forward(upstream, client)
|
||||
client.close()
|
||||
upstream.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--listen", required=True, help="local bind, e.g. 127.0.0.1:4404")
|
||||
ap.add_argument("--target", required=True, help="forward target, e.g. docker.local:4404")
|
||||
args = ap.parse_args()
|
||||
|
||||
lhost, lport = args.listen.rsplit(":", 1)
|
||||
thost, tport = args.target.rsplit(":", 1)
|
||||
target_addr = (thost, int(tport))
|
||||
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind((lhost, int(lport)))
|
||||
listener.listen(16)
|
||||
print(f"forwarding {args.listen} -> {args.target}", flush=True)
|
||||
|
||||
while True:
|
||||
client, _ = listener.accept()
|
||||
threading.Thread(target=_handle, args=(client, target_addr), daemon=True).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user