From 9ef21c1fb8749afc99effeae224aae616cd3d895 Mon Sep 17 00:00:00 2001 From: Oleksandr Shyshatskyi Date: Sun, 30 Aug 2026 22:17:00 +0300 Subject: [PATCH] wpa_supplicant is a supervised longrun, not a oneshot that forks The wifi oneshot backgrounded a subshell and returned at once, so that the 15s wait for wlan0 and the 20s wait to associate did not stall the boot transition firmwareExe blocks on. Its comment was explicit that nothing could depend on it, which was true and was not the problem. The problem is the subshell it left behind: a child of s6rc-oneshot-runner with its stdout on a pipe s6-rc closes the moment the up script returns, so a sleep in the wlan0 wait followed by an echo is a write to a closed pipe. OBSERVED on a printer: a healthy supervision tree, s6-rc reporting wifi up, no wlan0 configured, no lease, and not one line anywhere saying why -- HelixScreen calling it "wifi system is not ready" because there was no supplicant holding a control socket. Getting in at all needed a repacked .tgz that brought wifi up ahead of s6. The subshell was never caught in the act and could not be: once its pipe is gone it cannot report its own death. Which is the argument, whatever the exact signal was. A oneshot that forks and returns cannot be relied on and cannot be debugged when it isn't. So: type longrun, no -B, exec wpa_supplicant in the foreground. Blocking IS the job now, s6-supervise holds the process, and a supplicant that dies is restarted rather than mourned. up/down are gone -- a longrun's down is silently dropped anyway. wifi-dhcp is new and is not optional. wpa_supplicant only associates; it never asks for an address, and running udhcpc was the oneshot's last act. bin/wifi-action.sh has shipped since the s6 migration for exactly this hook and was invoked by nothing at all -- `wpa_cli -a` is what invokes it. That also fixes what the oneshot never covered: it ran udhcpc ONCE after boot, so an owner joining a network from the HelixScreen panel an hour later was associated with no address, which the panel reports as a connection timeout. Its run script re-runs the action script when wpa_state is already COMPLETED. wpa_cli fires on CHANGES, and it is not established that it emits CONNECTED on attach to an already-connected supplicant, so without this the lease that finish drops would not come back on a restart. The action script replaces any previous client first, so it is idempotent with a real event. udhcpc keeps -b inside the action script. Different flag, different program: wpa_cli blocks until the script returns, so a foreground udhcpc would wedge event processing. ntp/run reasoned about "the wifi oneshot" and about nothing being allowed to depend on it; it still takes no wifi dependency, now because wifi being up means a supplicant is running rather than that anything is routable. The service tables in how-it-works.md and building.md, and test_s6rc.py -- where wifi sat in HW_SET as "the oneshot above" and test_execline_shipped_too used it as its example -- follow. Both wifi and wifi-dhcp stay out of BOOT_SET: they want a real wlan0, which the replica has not got. qa-static passes. The database is NOT compiled here -- s6-rc-compile is a MIPS binary that runs in the replica -- so make qa-replica is still owed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QPQqLMitZzrRcaehiYLHdx --- docs/building.md | 6 +- docs/how-it-works.md | 4 +- .../payload/etc/s6-rc/source/ntp/run | 15 ++- .../s6-rc/source/ok-all/contents.d/wifi-dhcp | 0 .../source/wifi-dhcp/dependencies.d/wifi | 0 .../payload/etc/s6-rc/source/wifi-dhcp/finish | 14 +++ .../payload/etc/s6-rc/source/wifi-dhcp/run | 69 ++++++++++++++ .../payload/etc/s6-rc/source/wifi-dhcp/type | 1 + .../payload/etc/s6-rc/source/wifi/down | 16 ---- .../payload/etc/s6-rc/source/wifi/run | 94 +++++++++++++++++++ .../payload/etc/s6-rc/source/wifi/type | 2 +- .../payload/etc/s6-rc/source/wifi/up | 58 ------------ qa/replica/test_s6rc.py | 15 +-- 13 files changed, 201 insertions(+), 93 deletions(-) create mode 100644 pkgs/anvil-core/payload/etc/s6-rc/source/ok-all/contents.d/wifi-dhcp create mode 100644 pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/dependencies.d/wifi create mode 100755 pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/finish create mode 100755 pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/run create mode 100644 pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/type delete mode 100644 pkgs/anvil-core/payload/etc/s6-rc/source/wifi/down create mode 100755 pkgs/anvil-core/payload/etc/s6-rc/source/wifi/run delete mode 100644 pkgs/anvil-core/payload/etc/s6-rc/source/wifi/up diff --git a/docs/building.md b/docs/building.md index 0ddb61a..df5d6db 100644 --- a/docs/building.md +++ b/docs/building.md @@ -292,9 +292,9 @@ pkgs/anvil-core/ what makes the machine ours, and nothing else payload/ anvil-env.sh PATH/LD_LIBRARY_PATH/FF_PYTHON -- sourced bin/ ff-startup.py, ff_mcu_bringup.py, ffscreen.py, wifi-action.sh - etc/s6-rc/source/ one directory per service: wifi, nginx, moonraker, - camera, klipper, ui, mcu-bringup, ff-startup, - ok-all + etc/s6-rc/source/ one directory per service: wifi, wifi-dhcp, + nginx, moonraker, camera, klipper, ui, ntp, + mcu-bringup, ff-startup, ok-all nginx/nginx.conf bin/anvil-link-prog.sh links the two below, and the configs, into the absolute paths the stock scripts read diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 7f410bc..5756a7a 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -54,7 +54,9 @@ compiled into a database at build time and supervised by `s6-svscan`: ``` mcu-bringup oneshot toolhead boards out of their bootloaders klipper longrun klippy (depends on mcu-bringup) -wifi oneshot wlan0 + wpa_supplicant + udhcpc +wifi longrun wpa_supplicant on wlan0 +wifi-dhcp longrun udhcpc, on wpa_supplicant's association + events (depends on wifi) nginx longrun nginx (Mainsail) on :80 moonraker longrun moonraker on :7125 camera longrun mjpg-streamer on :8080 (nginx proxies it at /webcam/) diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/ntp/run b/pkgs/anvil-core/payload/etc/s6-rc/source/ntp/run index 131a714..70b276d 100755 --- a/pkgs/anvil-core/payload/etc/s6-rc/source/ntp/run +++ b/pkgs/anvil-core/payload/etc/s6-rc/source/ntp/run @@ -11,19 +11,18 @@ # # A LONGRUN, NOT A ONESHOT AFTER WIFI, and the difference is the whole design: # -# * The wifi oneshot runs ONCE, at boot, and returns immediately (read its -# `up`: the waits are backgrounded on purpose). A time sync hung off the -# end of it would fire while wlan0 was still associating, fail, and never -# be tried again -- and would not fire at all for the printer whose owner -# joins a network from the HelixScreen panel an hour after boot. +# * A time sync hung off the end of `wifi` would fire while wlan0 was +# still associating, fail, and never be tried again -- and would not fire +# at all for the printer whose owner joins a network from the HelixScreen +# panel an hour after boot. # * A clock is not set once. There is no RTC to hold the answer, so the # correction has to be maintained for as long as the printer is up. # # sntpd does both by design: with the network down it logs "will try again # later" and keeps its schedule, rather than exiting, so THIS SERVICE NEEDS NO -# WIFI DEPENDENCY -- which is just as well, because nothing may depend on the -# wifi oneshot (see its `up`). It syncs whenever the network turns up, from -# whichever direction. +# WIFI DEPENDENCY -- and would gain nothing from one, because `wifi` up means +# a supplicant is running, not that anything is routable. It syncs whenever +# the network turns up, from whichever direction. MODDIR=/usr/data/anvil # anvil-env.sh is read HERE, not inherited. s6-supervise starts this script diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/ok-all/contents.d/wifi-dhcp b/pkgs/anvil-core/payload/etc/s6-rc/source/ok-all/contents.d/wifi-dhcp new file mode 100644 index 0000000..e69de29 diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/dependencies.d/wifi b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/dependencies.d/wifi new file mode 100644 index 0000000..e69de29 diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/finish b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/finish new file mode 100755 index 0000000..f228285 --- /dev/null +++ b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/finish @@ -0,0 +1,14 @@ +#!/bin/sh +# Runs after wpa_cli exits -- on every stop, and on every restart. +# +# The action script's DISCONNECTED branch drops the lease already, but only +# when there is a wpa_cli alive to be told. A supplicant killed outright -- an +# OOM, a kill -9, `s6-rc -d change wifi` -- produces no event at all, and the +# udhcpc left behind goes on renewing an address for a network this printer is +# no longer associated to. That is worse than having no address, because it +# looks like success to everything that checks. +# +# Safe on a restart rather than a stop because `run` asks the supplicant +# whether it is still associated and re-runs the action script if it is. +killall udhcpc 2>/dev/null +exit 0 diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/run b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/run new file mode 100755 index 0000000..2fafc19 --- /dev/null +++ b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/run @@ -0,0 +1,69 @@ +#!/bin/sh +# The DHCP client, hung off wpa_supplicant's own association events. +# Exec'd by s6-supervise, never by hand. +# +# WPA_SUPPLICANT ASSOCIATES AND NOTHING MORE; it never asks for an address. +# Stock firmwareExe watched for that and ran udhcpc itself (it is full of +# "udhcpc -i wlan0" strings) and FlashForge shipped an action script at +# /usr/prog/wifi/bin/wifi_reconnect for the same hook. Ours is +# $MODDIR/bin/wifi-action.sh, which has shipped since the s6 migration and, +# until this service existed, was invoked by nothing at all. +# +# WHY A SERVICE AND NOT TWO LINES IN wifi/run. The old wifi oneshot ran udhcpc +# ONCE, straight after association, which covers exactly one story: a printer +# booting in range of a network it already knows. It does not cover the owner +# who joins a network from the HelixScreen panel an hour later -- associated, +# no address, which the panel reports as a connection timeout -- and it does +# not cover a lease lost to an AP reboot. `wpa_cli -a` fires on every +# association change for as long as it runs, so all three become one path. +MODDIR=/usr/data/anvil +IFACE=wlan0 +ACTION=$MODDIR/bin/wifi-action.sh +CTRL=/var/run/wpa_supplicant/$IFACE + +[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh + +mkdir -p /usr/data/logs +exec >>/usr/data/logs/anvil-wifi.log 2>&1 + +if [ ! -x "$ACTION" ]; then + echo "wifi-dhcp: !! no $ACTION -- nothing will ask for an address" + sleep 5 + exit 1 +fi + +# wpa_cli attaches to the control socket and exits at once if it is not there. +# `dependencies.d/wifi` only says the supplicant was exec'd, not that it has +# created the socket -- wifi has no notification-fd and nothing it could +# honestly send one on -- so the wait belongs here. +n=0 +while [ $n -lt 30 ] && [ ! -e "$CTRL" ]; do + sleep 1 + n=$((n + 1)) +done +if [ ! -e "$CTRL" ]; then + echo "wifi-dhcp: !! $CTRL never appeared -- is wpa_supplicant up?" + sleep 5 + exit 1 +fi + +killall wpa_cli 2>/dev/null + +# THE RESTART CASE, and it is the one that bites. wpa_cli runs the action +# script on CHANGES; restarted while the printer is already associated there +# is no change left to fire on, and the lease `finish` just dropped would +# never come back. The action script replaces any previous client before it +# starts one, so calling it here is idempotent with an event that may also +# arrive a moment later. +if /usr/sbin/wpa_cli -i $IFACE status 2>/dev/null | grep -q '^wpa_state=COMPLETED'; then + echo "wifi-dhcp: already associated -- asking for a lease now" + "$ACTION" "$IFACE" CONNECTED +fi + +# NO -B, for the reason wifi/run has none. wpa_cli exits when the supplicant +# goes away, which is correct here rather than a fault: s6 restarts this, it +# waits for the new control socket, and the printer comes back without anyone +# typing anything. +# +# And `exec`, so a stop signals wpa_cli rather than a shell that ignores it. +exec /usr/sbin/wpa_cli -i $IFACE -a "$ACTION" diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/type b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi-dhcp/type @@ -0,0 +1 @@ +longrun diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/down b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/down deleted file mode 100644 index dcaa8d3..0000000 --- a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/down +++ /dev/null @@ -1,16 +0,0 @@ -/bin/sh -c " -# By name: nothing here was started with a pidfile, and inventing one for a -# daemon that backgrounds itself would be a second thing that can be wrong. -killall wpa_cli 2>/dev/null -killall udhcpc 2>/dev/null -killall wpa_supplicant 2>/dev/null -# killall returns as soon as the signal is sent, so without this wait a -# reconnect a moment later finds the dying supplicant in the process table and -# does nothing. -n=0 -while [ \$n -lt 15 ] && { pgrep -f wpa_supplicant >/dev/null 2>&1 || ps 2>/dev/null | grep wpa_supplican[t] >/dev/null; }; do - sleep 1 - n=\$((n + 1)) -done -echo 'wifi: stopped' -" diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/run b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/run new file mode 100755 index 0000000..68e4ef6 --- /dev/null +++ b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/run @@ -0,0 +1,94 @@ +#!/bin/sh +# wpa_supplicant, as s6 supervises it. Exec'd by s6-supervise, never by hand. +# +# A LONGRUN, AND NO -B, and that pair is the whole point of this file. +# +# This was a oneshot whose `up` backgrounded a subshell and returned at once, +# so that the 15s wait for wlan0 and the 20s wait to associate did not stall +# the boot transition firmwareExe blocks on. The comment there was explicit +# that nothing could depend on it, which was true and not the problem. The +# problem is that the subshell it left behind was a child of +# s6rc-oneshot-runner with its stdout on a pipe s6-rc closes the moment the up +# script returns -- so a `sleep 1` in the wlan0 wait, then an `echo`, is a +# write to a closed pipe. +# +# OBSERVED: a printer that booted with a healthy supervision tree, `s6-rc` +# reporting wifi up, no wlan0 configured, no lease, and not one line anywhere +# saying why -- HelixScreen calling it "wifi system is not ready" because +# there was no supplicant to hold a control socket. The subshell was never +# caught in the act, and could not be: once its pipe is gone it has no way to +# report its own death. Which is the argument. A oneshot that forks and +# returns cannot be relied on and cannot be debugged when it isn't. +# +# A longrun has neither half of that. Blocking IS the job, s6-supervise holds +# the process, and a supplicant that dies is restarted rather than mourned. +MODDIR=/usr/data/anvil +IFACE=wlan0 + +# The saved networks. FlashForge's file: the mod neither ships nor touches it, +# so whatever their UI saved is still what this reads. +CONF=/usr/prog/wifi/wpa_supplicant.conf + +# anvil-env.sh is read HERE, not inherited. s6-supervise starts this script +# from the SCANNER's environment, which is firmwareExe's at boot but whatever +# an ssh session had when the service is restarted by hand. +[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh + +# The same log the action script writes, so association and address are one +# story in one file. Appending, so a respawn cannot erase the reason for it. +mkdir -p /usr/data/logs +exec >>/usr/data/logs/anvil-wifi.log 2>&1 +echo "=== wifi `date 2>/dev/null` ===" + +# app_startup.sh insmods 8821cu.ko just before it runs firmwareExe, so on a +# normal boot wlan0 is already here; the wait covers a slow USB enumeration +# and the restart typed over ssh. A run script cannot decline to run -- s6 +# respawns it -- so a missing interface is reported and slept on rather than +# exec'd into a loop that fills the log once a second. +n=0 +while [ $n -lt 30 ] && [ ! -d /sys/class/net/$IFACE ]; do + sleep 1 + n=$((n + 1)) +done +if [ ! -d /sys/class/net/$IFACE ]; then + echo "wifi: !! $IFACE never appeared -- is 8821cu.ko loaded?" + sleep 5 + exit 1 +fi + +# A MISSING CONFIG IS NOT PERMANENT, so this retries slowly instead of giving +# up the way the oneshot did. It appears the first time a network is saved. +# Note the standing chicken-and-egg, unchanged by this file and not ours to +# fix here: a printer with no config has no supplicant, so it has no control +# socket for HelixScreen to add a network through either. +if [ ! -f "$CONF" ]; then + echo "wifi: !! no $CONF -- no saved network to join" + sleep 60 + exit 1 +fi + +# s6-supervise runs one copy of this at a time, so any supplicant answering +# now is a leftover -- one started by hand over ssh, or by the rescue block an +# older firmwareExe carried. Two supplicants on one interface fight over the +# association and the loser's retries look like a flapping AP. +killall wpa_supplicant 2>/dev/null +n=0 +while [ $n -lt 15 ] && { pgrep -f wpa_supplicant >/dev/null 2>&1 \ + || ps 2>/dev/null | grep wpa_supplican[t] >/dev/null; }; do + sleep 1 + n=$((n + 1)) +done + +/sbin/ifconfig $IFACE up 2>/dev/null +mkdir -p /var/run/wpa_supplicant + +# NO -B: with it, s6 would hold the parent that forked and exited, call the +# service dead and respawn it for ever beside a real supplicant supervised by +# nobody -- the same trap nginx's `daemon off;` avoids. +# +# nl80211 first, wext as the fallback: the 8821cu is a Realtek USB part and +# which one it registers varies by build. +# +# And `exec`, so the SIGTERM behind `s6-rc -d change wifi` reaches the +# supplicant rather than a shell that ignores it. +exec /usr/sbin/wpa_supplicant -i $IFACE -c "$CONF" -D nl80211,wext diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/type b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/type index bdd22a1..5883cff 100644 --- a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/type +++ b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/type @@ -1 +1 @@ -oneshot +longrun diff --git a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/up b/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/up deleted file mode 100644 index 3d176ee..0000000 --- a/pkgs/anvil-core/payload/etc/s6-rc/source/wifi/up +++ /dev/null @@ -1,58 +0,0 @@ -/bin/sh -c " -# wlan0 + wpa_supplicant + udhcpc. Nothing in /etc/init.d brings the interface -# up: FlashForge's firmwareExe owned the whole network stack, and app_startup.sh -# only insmods the 8821cu driver. So the moment the mod replaces firmwareExe the -# printer boots with no ssh, no Mainsail and no way to diagnose either. -# -# A ONESHOT, not a longrun: wpa_supplicant backgrounds itself with -B and so -# does udhcpc, so there is no process for s6-supervise to hold. -# -# THE WAITS ARE BACKGROUNDED and this returns at once, because a oneshot's up -# runs inside the transition firmwareExe is blocked on -- up to 15s for wlan0 to -# register after the insmod and 20s more to associate, and that delay would land -# on exactly the printer with no network. Returning early is lying to whatever -# depends on this, so NOTHING DEPENDS ON WIFI, deliberately: moonraker and nginx -# bind 0.0.0.0 and klipper talks to serial ports. -# -# The saved networks live in /usr/prog/wifi/wpa_supplicant.conf, which the mod -# never ships or touches, so whatever FlashForge's UI saved is still there. -IFACE=wlan0 -CONF=/usr/prog/wifi/wpa_supplicant.conf - -if pgrep -f wpa_supplicant >/dev/null 2>&1 || ps 2>/dev/null | grep wpa_supplican[t] >/dev/null; then - echo 'wifi: wpa_supplicant already running' - exit 0 -fi -[ -f \"\$CONF\" ] || { echo \"wifi: !! no \$CONF -- nothing to connect to\"; exit 0; } - -( - n=0 - while [ \$n -lt 15 ] && [ ! -d /sys/class/net/\$IFACE ]; do - sleep 1 - n=\$((n + 1)) - done - if [ ! -d /sys/class/net/\$IFACE ]; then - echo \"wifi: !! \$IFACE never appeared -- is 8821cu.ko loaded?\" - exit 1 - fi - /sbin/ifconfig \$IFACE up 2>/dev/null - mkdir -p /var/run/wpa_supplicant - # nl80211 first, wext as the fallback: the 8821cu is a Realtek USB part and - # which one it registers varies by build. - if /usr/sbin/wpa_supplicant -B -i \$IFACE -c \"\$CONF\" -D nl80211,wext; then - echo 'wifi: wpa_supplicant started' - else - echo 'wifi: !! wpa_supplicant failed to start' - exit 1 - fi - n=0 - while [ \$n -lt 20 ]; do - /usr/sbin/wpa_cli -i \$IFACE status 2>/dev/null | grep -q '^wpa_state=COMPLETED' && break - sleep 1 - n=\$((n + 1)) - done - /sbin/udhcpc -i \$IFACE -b -t 5 >/dev/null 2>&1 & - echo \"wifi: udhcpc requesting a lease on \$IFACE\" -) & -echo 'wifi: bringing up wlan0 in the background' -" diff --git a/qa/replica/test_s6rc.py b/qa/replica/test_s6rc.py index 49a5422..46fd99e 100644 --- a/qa/replica/test_s6rc.py +++ b/qa/replica/test_s6rc.py @@ -23,8 +23,9 @@ DB = MODDIR + "/etc/s6-rc/compiled/current" # sets are in ok-all on a real printer. # # BOOT_SET is the longruns that need nothing but the rootfs, so s6-supervise -# must report them up here. wifi is NOT in it despite needing no hardware: it -# is a oneshot, so there is no supervised servicedir for s6-svstat to read. +# must report them up here. wifi and wifi-dhcp are longruns now and do have +# supervised servicedirs, but they are NOT in it: both want a real wlan0, and +# on a machine without one they report the interface missing and exit. BOOT_SET = {"nginx", "moonraker", "ntp"} # HW_SET cannot come up on a replica, so it is asked of the compiled database # instead -- still where a missing or misnamed service shows: @@ -36,8 +37,10 @@ BOOT_SET = {"nginx", "moonraker", "ntp"} # the live list # ui needs no hardware itself, but depends on ff-startup, so the # transition never reaches it -# wifi the oneshot above -HW_SET = {"camera", "klipper", "mcu-bringup", "ff-startup", "ui", "wifi"} +# wifi wants /sys/class/net/wlan0 and the 8821cu behind it +# wifi-dhcp wants wifi's control socket, so it follows wifi down +HW_SET = {"camera", "klipper", "mcu-bringup", "ff-startup", "ui", "wifi", + "wifi-dhcp"} # EVERY s6-rc-init HERE NEEDS THIS, for the reason firmwareExe needs it: the # default deadline is TAIN_INFINITE_RELATIVE, which does not fit this printer's @@ -157,8 +160,8 @@ def test_the_supervisor_is_the_one_we_shipped(box): def test_execline_shipped_too(box): """Every oneshot's `up` runs through s6rc-oneshot-runner, whose `run` is - execline. Without execlineb the wifi oneshot fails at exec and nothing - else does, which reads as a wifi bug.""" + execline. Without execlineb mcu-bringup and ff-startup fail at exec while + every longrun starts fine, which reads as a klipper bug.""" assert box.file(MODDIR + "/bin/execlineb").executable