diff --git a/scripts/meshtasticator_e2e/README.md b/scripts/meshtasticator_e2e/README.md index eebbb24..78e3710 100644 --- a/scripts/meshtasticator_e2e/README.md +++ b/scripts/meshtasticator_e2e/README.md @@ -85,11 +85,16 @@ simulator. Nothing is forwarded when the daemon is local (unset/unix on the forwarders themselves. Other headless adaptations the script makes automatically: -* patches `lib/gui.py` `matplotlib.use("TkAgg")` → `"Agg"` in the pinned - Meshtasticator checkout — the TkAgg call crashes headless runs even under - `MPLBACKEND=Agg` (only the interactive GUI mode is lost), +* patches `lib/gui.py` in the pinned Meshtasticator checkout: forces the Agg + backend (the code calls `matplotlib.use("TkAgg")` at import) and makes + `move_figure()` a no-op when no Tk window manager exists (its unconditional + `canvas.manager.window` access crashes under Agg at Graph construction). + Only the interactive GUI modes, never used by this harness, are lost, * warms the `meshtastic/meshtasticd` image (`docker pull`) before booting the - simulator so the pull is not on the simulator's node-boot critical path. + simulator so the pull is not on the simulator's node-boot critical path, +* feeds the simulator's interactive `cmdloop` from a FIFO held open until + teardown — an EOF stdin (e.g. CI) would otherwise make it exit right after + booting the nodes. Without `MESHTASTICATOR_E2E=1` the e2e module skips, so ordinary unit/CI runs stay fast and gateway-free. diff --git a/scripts/meshtasticator_e2e/run_e2e.sh b/scripts/meshtasticator_e2e/run_e2e.sh index a4fe846..3a527dc 100755 --- a/scripts/meshtasticator_e2e/run_e2e.sh +++ b/scripts/meshtasticator_e2e/run_e2e.sh @@ -109,14 +109,33 @@ else PROBE_HOST="$HOST" fi -# The simulator forces TkAgg at import (lib/gui.py: matplotlib.use("TkAgg")), -# which crashes headless runs ("Tkinter is needed") even under MPLBACKEND=Agg — -# an explicit use() overrides the env var. Neutralize it in the pinned -# checkout; only the interactive GUI mode (never used by this harness) is lost. -if grep -q 'matplotlib.use("TkAgg")' "$SIM_DIR/lib/gui.py"; then - echo "== patching meshtasticator lib/gui.py: TkAgg -> Agg (headless) ==" - sed -i.bak 's/matplotlib.use("TkAgg")/matplotlib.use("Agg")/' "$SIM_DIR/lib/gui.py" -fi +# The simulator assumes an interactive Tk desktop: lib/gui.py forces +# matplotlib.use("TkAgg") at import and move_figure() touches the Tk window +# manager at Graph construction — both crash headless runs even under +# MPLBACKEND=Agg. Neutralize them in the pinned checkout (only the interactive +# GUI modes, never used by this harness, are lost). +echo "== patching meshtasticator lib/gui.py for headless runs ==" +"$VENV/bin/python" - "$SIM_DIR" <<'PY' +import sys +from pathlib import Path + +gui = Path(sys.argv[1]) / "lib/gui.py" +src = gui.read_text() + +src = src.replace('matplotlib.use("TkAgg")', 'matplotlib.use("Agg")') + +old = 'def move_figure(fig, x, y):\n fig.canvas.manager.window.wm_geometry("+%d+%d" % (x, y))' +new = ('def move_figure(fig, x, y):\n' + ' # Headless (Agg) backends have no Tk window manager.\n' + ' try:\n' + ' fig.canvas.manager.window.wm_geometry("+%d+%d" % (x, y))\n' + ' except AttributeError:\n' + ' pass') +assert old in src, "move_figure source no longer matches the pinned meshtasticator" +src = src.replace(old, new) +gui.write_text(src) +print("patched lib/gui.py") +PY if [ "$MODE" = docker ]; then # Warm the node image on the (possibly remote) daemon first: the simulator @@ -127,14 +146,22 @@ if [ "$MODE" = docker ]; then fi echo "== booting Meshtasticator ($MODE) with $NODES node(s) from $SIM_DIR ==" +# The simulator ends in an interactive cmdloop reading stdin; an EOF there +# (e.g. CI) would make it exit right after booting the nodes. Feed it from a +# FIFO whose write end this shell keeps open until cleanup. +SIM_FIFO="$(mktemp -u /tmp/meshtasticator-XXXXXX.fifo)" +mkfifo "$SIM_FIFO" ( cd "$SIM_DIR" - "$SIM_PYTHON" interactiveSim.py "${SIM_ARGS[@]}" >"$SIM_LOG" 2>&1 + "$SIM_PYTHON" interactiveSim.py "${SIM_ARGS[@]}" <"$SIM_FIFO" >"$SIM_LOG" 2>&1 ) & SIM_PID=$! +exec 9>"$SIM_FIFO" # hold the write end open -> sim stdin never hits EOF +rm -f "$SIM_FIFO" cleanup() { echo "== tearing down simulator (pid $SIM_PID) ==" + exec 9>&- 2>/dev/null || true # release the FIFO write end (sim stdin EOF) kill "$SIM_PID" 2>/dev/null || true wait "$SIM_PID" 2>/dev/null || true for pid in "${FWD_PIDS[@]:-}"; do