Moonraker is no longer copied to /usr/prog. The payload already extracts
that exact tree to /usr/data/anvil/moonraker, so the installer was making a
second identical copy onto the small firmware partition -- the one install
step that could fail on space, whose failure was "this printer has no
working web UI". Worse, /usr/prog is what a stock flash overwrites while
/usr/data survives, so a firmware update silently reverted Moonraker to
FlashForge's 2022 build and left the rest of the mod in place: exactly the
"which Moonraker is this printer running?" ambiguity the previous commit set
out to end. Neither of the two things thought to pin it there holds -- the
moonraker-env virtualenv is not on sys.path (imports resolve from
Python-3.8.2/lib/python3.8/site-packages, checked on the real image) and
moonrakerDaemon is never invoked. FlashForge's tree is left alone, unused.
S60web became S60nginx and S62moonraker. nginx and moonraker fail for
different reasons and are now stopped, started and diagnosed separately;
MOD_WEB still governs both.
payload/anvil-service.sh is the shape all six services now share. They had
been written one at a time and had arrived at four different answers to "is
it running" and two to "restart", none of them decisions. Two real bugs were
living in that drift, and both are fixed here:
* busybox start-stop-daemon -K returns before the process is dead and
never removes the pidfile, so restart raced its own start -- stop
returned, start found the pidfile still held, and the service ended up
stopped rather than restarted. The `sleep 2` in front of restart was
standing in for that wait; svc_stop_daemon does it properly and the
sleep is gone.
* klippy does not unlink /tmp/uds on exit. supervise() knew that; stop()
did not, so status reported a dead klippy as running and restart
declined to do anything at all.
run-append.sh now clears $MODDIR/init.d before extracting. Without it the
rename above would leave a stale S60web beside the two new scripts on any
printer that already has the mod, and firmwareExe runs every executable
S* in filename order -- starting nginx and moonraker twice, from scripts
disagreeing about where moonraker lives.
bin/patch.sh stages anvil-service.sh. It is sourced, so no chmod +x.
test/integration/printer/case-services.sh is a new replica gate: it runs
every service on the printer's own busybox and checks the contract they
share rather than grepping them for how they are spelled. case-moonraker.sh
now asserts moonraker runs from /usr/data, that nothing under /usr/prog is
executed, that restart yields a genuinely new pid, and that stopping
moonraker leaves nginx up -- the point of the split.
The .modold rollback recovery in docs/how-it-works.md described a code path
removed in the previous commit and is rewritten for what happens now.
Verified in the replica (real rootfs.squashfs, qemu-mipsel): case-moonraker,
case-services, case-mcu-bringup and case-boot-screen all pass; pytest 181
passed, 5 skipped. case-install.sh and case-recovery.sh are syntax-checked
only -- they need a stock FlashForge package, which is not in this checkout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
The printer replica
Everything in test/integration/printer/ exists to answer one question honestly: would
this package brick the machine?
A Debian container with hand-written stand-ins cannot answer it. The stock
installer is shell, and shell behaviour is not portable in the ways that
matter here — Debian's /bin/sh is dash, which rejects
${file_name:${#start_head}:${version_length}} with Bad substitution, while
the printer's busybox accepts it. Get that wrong in either direction and the
simulation and the printer disagree about whether an update installs at all.
So the replica runs the printer's own userland.
What it is
work/rootfs/ rootfs.squashfs, extracted from the stock package's
kernel component -- the printer's real root filesystem
(busybox 1.31.1, glibc 2.33, MIPS32r2 nan2008, mipsel)
test/integration/printer-exec.py starts a privileged container that:
- registers a
binfmt_mischandler so MIPS binaries execute (see below), - builds the machine's mount layout,
- installs the stock FlashForge package with FlashForge's own installer —
only when
BASE_PKGis set.make test-mcudoes not set it, so the MCU gate skips this step, chroots in and runs the test case.
From step 4 onward there is no host shell involved. sh, tar, md5sum,
find, cmp and the unTar decryptor are all the printer's binaries,
executing under qemu-mipsel (expr and unzip are the printer's too, but
are reached only from inside the stock installer, not from any case script).
The mount layout
Copied from the machine (/etc/fstab, /etc/init.d/S09mount_mmc_prog,
/etc/init.d/S21mount_mmc_ext4):
| path | what it is | mode |
|---|---|---|
/ |
rootfs.squashfs |
read-only |
/usr/prog |
ext4 usershare partition |
rw |
/usr/data |
ext4 userdata partition |
rw |
/tmp, /run |
tmpfs | rw |
/mnt |
the USB stick | rw |
Root is read-only on purpose. On the printer a write to /bin or /etc
silently fails; a mod that depended on one would pass a permissive sandbox and
fail on the machine.
/dev is a tmpfs holding null, zero, full, random, urandom, tty,
a devpts at pts, a tmpfs at shm, and a regular file for fb0 (the
installer does cat start.img > /dev/fb0). /sys is mounted read-only.
The one block device is the USB stick. With USB_STICK=1 the harness formats
a FAT filesystem, copies the packages into it, attaches it to a loop device
and exposes it as /dev/sda1 — the name app_startup.sh looks for. That is
what lets the end-to-end test run the boot script verbatim instead of
re-implementing it: the printer mounts the stick itself, globs for its own
filename pattern, and decides on its own whether there is an update. Nothing
that could reach an eMMC partition (mmcblk*) exists, so a stray write still
has nowhere to go.
app_startup.sh mounts the stick with -o,codepage=936,iocharset=utf8. The
printer's kernel has nls_cp936 built in; container kernels usually do not,
so /bin/mount is wrapped to try the genuine options first and retry without
codepage= only if the kernel rejects them. The retry is recorded in
/usr/prog/.SIMULATED and in the neutered-calls log the test prints.
The one-byte reason this did not work before
qemu-user-static registers a binfmt matcher for mipsel whose mask requires
e_ident[EI_ABIVERSION] — byte 8 of the ELF header — to be 0. Every binary
the Ingenic toolchain produced for this printer has 3 there:
00000000: 7f45 4c46 0101 0100 0300 0000 0000 0000 .ELF............
^^ EI_ABIVERSION = 3
So the stock handler never matched, and every attempt to run a printer binary
came back exec format error — with qemu installed, registered and enabled.
test/integration/printer/binfmt.sh registers a handler with that byte masked out.
Everything else is the stock mipsel matcher.
The handler is registered with the F (fix-binary) flag, so the kernel holds
the interpreter open and it resolves inside the chroot without copying qemu
into the printer's filesystem.
Getting rid of the stubs entirely
/usr/prog is a factory image: update packages carry only slices of it, and
the full thing ships in the factory firmware (see below — it is published).
Two things supply it to the replica, and there is no third:
-
PROG_DUMP— a real/usr/progoff a printer, used verbatim:ssh root@printer 'tar -cf - /usr/prog' > prog.tar # then in test.env: PROG_DUMP="/path/to/prog.tar"With one, nothing below is stubbed: the klipper daemons,
nginx,python3,checkEboardand the printer's own OpenSSL 1.0.2d all become genuine, and the-md md5substitution disappears because the real 1.0.2d binary runs under qemu. This is the single highest-value artefact you can hand the test suite. -
FlashForge's own installer, run inside the replica, which installs the parts an update package does carry on top of it.
There used to be a third: stubs, for whatever neither of those provided. They
are gone. A green run that came from a hand-written python3 and an OpenSSL 3
pretending to be 1.0.2d is reassurance, not evidence — test/integration/printer/seed-prog.sh
now hard-fails when the prog partition is not real.
A full-filesystem factory image works too, and one is published:
https://github.com/ghzserg/FF/releases/download/R/Creator5Pro-factory.tar.xz
md5 d2fdc0e1deb17c41cbb6016d55ab3031 182MB compressed, 828MB extracted
It is the genuine /usr/prog and /usr/data off a Creator 5 Pro —
Python-3.8.2, nginx, openssl-1.0.2d, moonraker, qt-4.8.6,
ffmpeg-4.0.2, opencv-4.10, the klipper tree, firmwareRes, the stock
configs. Point PROG_DUMP (in test.env) at the .tar.xz directly; the replica works out
whether an archive is a whole-filesystem image or the contents of /usr/prog
and unpacks it accordingly.
With it, the replica decrypts packages using the printer's own OpenSSL
1.0.2d binary under qemu — so the -md md5 packing is verified against the
real implementation rather than against an assumption about it.
What is authentic and what is not
/usr/prog on a real printer is the factory image plus every update
installed since. The replica is assembled the same way — the published
factory image supplies the partition, and FlashForge's own installer lays
the update package on top:
- Installed for real, by FlashForge's own
run.sh:app_startup.sh,klipper/(klippy, extras, kinematics,chelper/c_helper.so),start.sh,klipper_pri.sh,unTar,wakeup_level,firmwareExe,passwd,shadow,modules/,PROGRAM/{software,library}/<version>/, plusffmpeg-402and thefirmwareResimage set from the library component. - Genuine, from the factory image: everything else on the prog partition —
klipperDaemon,moonrakerDaemon,checkEboard,nginx,python3,moonraker, the stocknginx.conf, and the printer's OpenSSL 1.0.2d. (A mod install leaves all of this alone. Its own Moonraker is extracted to/usr/data/anvil/moonrakeron the DATA partition and started from there — see how-it-works — so the factory tree here, the interpreter andmoonrakerDaemonall stay stock, andmoonrakerDaemonis simply never invoked. Nothing the mod installs is written to the prog partition'smoonraker/, which is what makes it a useful baseline:case-install.shasserts this tree is untouched after an install.) - Not present at all: stubs. Without a real prog partition the replica refuses to start rather than substitute one.
- Neutered, always:
insmod,rmmod,modprobe,reboot,poweroff,haltandcmd_mcu. The first six would act on the host kernel;cmd_mcuis called by klipper'sstart.shascmd_mcu write_firmware, and the genuine binary is neutered in place rather than shadowed, so nothing depends on PATH order.
Every one of these is listed inside the replica at /usr/prog/.SIMULATED, so
a test can never quietly depend on a stub without it being visible.
The baseline install deliberately skips the kernel-* and control-*
components: one rewrites eMMC partitions, the other flashes MCUs over
/dev/ttyS*. Neither can do anything useful in a container, and both are
destructive if they ever found real hardware.
Why not full-system emulation
The obvious next step would be qemu-system-mipsel booting the printer's own
kernel. It does not help. The kernel in the stock package is built for the
Ingenic X2000 SoC and there is no QEMU machine for it; booting the userland
under qemu-system-mipsel would mean supplying some other mipsel kernel
(malta, say), which is no more the printer's kernel than the host's is. What
it would cost is a boot cycle per test and a second kernel to maintain.
The install path is file operations and shell, and those run on the printer's own binaries here. The host kernel serves the syscalls; the printer's kernel is 5.6 and the host's is newer, which is a real difference but not one the installer can see. Where it would matter — module loading, MTD writes, MCU serial — the replica refuses to pretend, and those calls are neutered and logged instead.
What it still cannot tell you
- Nothing here drives the screen, the MCUs, the toolchanger or a real print.
The replica proves the install is safe and the machine would boot; the
hardware procedure in
docs/hardware-testing.mdis still how you find out whether it prints. firmwareExeis a 20MB Qt binary that wants a framebuffer. It is checked for presence, executability and ELF-ness, never run.- Timing-dependent boot races are not modelled.
- Partition sizes are unbounded unless you set
PROG_MBandDATA_MB(intest.env) from whatdf -hreports on the printer itself. Until then an install that runs the data partition out of space passes here and fails on the machine. The replica says so in/usr/prog/.SIMULATED.
Running it
make rootfs # once -- extracts rootfs.squashfs from the stock package
make test-install # install the built package into the replica
make test-recovery # mod in, stock package back out
make test-mcu # ff_mcu_bringup.py on the printer's own Python
make test-py # klipper config, and rootfs paths when one is present
make test # all of it
make test skips the replica half with a loud message if no stock package is
configured. REQUIRE_PRINTER_SIM=1 turns that skip into a failure — which is
what .github/workflows/release.yml sets, so a release cannot ship without
it.