fix(e2e): headless Graph construction + keep simulator cmdloop alive

Run 10537 reached node boot but the simulator crashed again at Graph
construction: lib/gui.py move_figure() unconditionally touches the Tk window
manager (canvas.manager.window), which does not exist under Agg. Patch it to a
no-op guard alongside the TkAgg->Agg backend swap. Also feed the simulator's
interactive cmdloop from a FIFO held open until teardown — CI stdin is EOF, so
the sim would otherwise exit immediately after booting the nodes.
This commit is contained in:
2026-09-07 21:41:50 -07:00
parent ba31d79368
commit eff352b2ea
2 changed files with 45 additions and 13 deletions
+9 -4
View File
@@ -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.
+36 -9
View File
@@ -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