Skip to content

Fix the nine issues found testing Reflash on an A8 (#149-#157) - #158

Merged
eliasbakken merged 12 commits into
mainfrom
fix-test-findings-149-157
Sep 17, 2026
Merged

eliasbakken merged 12 commits into
mainfrom
fix-test-findings-149-157

Conversation

@eliasbakken

Copy link
Copy Markdown
Contributor

Fixes the nine issues found by using Reflash on an A8 (s/n 0498): #149, #150, #151, #152, #153, #154, #155, #156, #157.

Each fix has a test that fails on main and passes here. make test passes: Go, 85 bats, 123 vitest.

The cancels

Most of these were one bug wearing different hats: a cancel changed the state, and something else changed it back.

  • Cancelling a backup ends in ERROR instead of CANCELLED #152 Cancelling a backup ended in ERROR. cancelBackup set CANCELLED, the next get_progress poll turned that into IDLE before the killed pipeline had exited, and goBackup then saw no cancel and recorded an error. goInstall and goMagic never checked for a cancel at all, so a cancelled install or magic did the same. A cancel now only asks - it sets a flag and kills xz - and the job reports CANCELLED once it has exited and cleaned up.
  • Install/Backup button sends a cancel while a download or upload is running #156 The kill was pkill -f xz -9, which matched any process with "xz" in its command line. Jobs now run in their own process group and the cancel reaches only that group's xz. Cancelling with nothing running is a no-op instead of an error toast.
  • Cancelling a file upload reports a USB drive failure and leaves the drive mounted rw #151 Cancelling an upload closed the file while a chunk was still writing. The write failed with "invalid argument", which set ERROR with "The upload failed while writing to the USB drive" - two red toasts for pressing Cancel - and because the state was ERROR the drive was never remounted read-only. upload_cancel now waits for the chunk in flight, then cleans up.
  • Download cancel (found on the way, no issue): cancel stopped only the wait. The copy ran on in a goroutine nothing stopped, downloading into the removed file until it was complete - hence "target is busy" on the remount and progress lines after the cancel. A failed download also panicked the server.

The leftovers

The rest

Two extras

  • tests: stop slowInit's watchdog from outliving its test - the Go suite already failed about 1 run in 4 on main, because a test left the watchdog running and it changed the shared state underneath later tests. 0 in 10 after.
  • wifi-connect: check the hotspot came back, and retry once - while testing a wrong passphrase, iwd 3.8 segfaulted during the restore and came back in station mode, leaving the board with no hotspot at all (WiFi credentials flow: unbounded polling against a dead origin, no hotspot fallback on failure #90's failure). Once in five attempts, and the same sequence without this branch's changes did not reproduce it either, so it looks like iwd rather than the caller - but the consequence is a board unreachable over Wi-Fi, so the restore now verifies and retries.

Checked on the board

A build of this branch was deployed to A8 0498 and driven through the same actions that produced the reports: upload cancel (clean, partial removed, drive ro), backup cancel (BACKUPING -> CANCELLED, partial removed), download cancel (no busy remount), a full download (100%, 332374256 of 332374256), delete (removed the images, refused ../options.cfg), the scan (signals from 1 to 4 stars), and a wrong passphrase in the browser: 67 seconds of "switching", then "Could not join Kraakeslottet", never "Connected", with the profile removed and the board back on its hotspot.

Not exercised on hardware: cancelling an install and the settings file written at the end of one, since both write the eMMC. Both have tests.

🤖 Generated with Claude Code

eliasbakken and others added 12 commits September 17, 2026 19:01
iwctl prints four stars for every network and draws the unlit ones in
grey (***\e[1;90m*\e[0m is three bars). Stripping the colour first made
every network read ****. Drop the grey stars before the colour codes.

Also drop AccessPoint.Frequency: iwctl's list has no frequency and
nothing ever filled it, so it was always "".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#150)

The profile is written before the attempt, with AutoConnect=true, and
was never removed. A wrong passphrase stayed in /var/lib/iwd and iwd
kept retrying it by itself (deauth reason 2, over and over), and a
mistyped retry overwrote a profile that worked.

Keep the old profile aside, and on failure forget the network in iwd
and put the old one back, or nothing if there was none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
watchForReconnect called it joined as soon as the board reported station
mode with the target SSID. network-status reads that SSID from iwctl's
Connected network, which is filled in while still connecting - so a wrong
passphrase showed "Connected to Kraakeslottet", the watch stopped, and
the fall back to the hotspot was never shown.

Ask the server, which already knows: wifi_poll_connect's error ends the
watch as a failure, and joined now also needs an address and an attempt
that is no longer in progress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cancelling a backup ended in ERROR: cancelBackup set CANCELLED, the next
get_progress poll turned that into IDLE before the killed pipeline had
exited, and goBackup then saw no cancel and recorded an error. Install
and magic-from-URL had the same shape - goInstall and goMagic never
checked for a cancel at all.

A cancel now sets a flag and kills xz in the job's own process group;
the goroutine, once the job has exited and been cleaned up after, sets
CANCELLED. Cancelling with nothing running is a no-op instead of an
error toast.

The kill was pkill -f xz -9, which matched anything with xz in its
command line. runWorker starts each job with Setpgid, and the cancel
only reaches xz in that group (#156).

A backup that is cancelled or fails now removes <label>.img.xz, the
file backup-emmc actually writes - cancelBackup removed <label>, which
never exists (#153). get_progress no longer remounts on CANCELLED: it
ran while the file was still open ("target is busy"), and the paths
that mount rw put the drive back themselves.

resetTransfer() clears every progress counter at each start (#155).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestStorageReadiness runs slowInit, which starts the 500ms watchdog, and
nothing stopped it. For the rest of the run it saved options and set the
shared state to SAVING and back to IDLE underneath whichever test was
running: TestControlSocket saw "STATE SAVING" and TestHandleSerialCommand
"STATE IDLE" for a state it had just set to MAGIC. origin/main failed 2
runs in 8; with startWatchdog stubbed in setupTest, 0 in 10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cancelling an upload closed state.File while a chunk admitted by
beginChunk was still writing. The write failed with "invalid argument",
failChunk set ERROR with "The upload failed while writing to the USB
drive", the user got two red toasts for pressing Cancel, and because
the state was ERROR the drive was never remounted read-only.

upload_cancel now refuses new chunks, waits (bounded) for the ones in
flight, closes the file, removes the partial image and remounts ro for
a plain upload, and only then sets CANCELLED. A chunk interrupted by the
cancel answers success=false instead of failing. The second
upload_cancel the client sends from that chunk is ignored instead of
putting CANCELLED back after the poll had moved on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cancel stopped only the wait. The copy ran on in a goroutine nothing
stopped, downloading into the removed file until the image was complete
- the remount afterwards failed with "target is busy", and progress
lines went on being logged after the cancel. The request is now bound to
the cancel context and the file is removed once the copy has returned.

A failed create or request used to panic the server; it is now ERROR,
and a non-200 answer is no longer saved as an image. cancel_download
with no download ever started no longer dereferences a nil cancelFunc.

The progress get_progress and STATUS report is sampled per poll, so a
finished download was left at 96%; it is set from the file at the end.
The bandwidth log no longer writes a rate for IDLE, and the JSON no
longer carries the Go file handle as "File": null.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…157)

/etc/rebuild-settings is sourced by bash as root on the new image, and
WIFI_SSID and WIFI_PSK were pasted between bare single quotes. A valid
passphrase like it's-my-wifi broke the file, and a crafted one ran
commands on first boot. Quote them properly. save-settings wrote the
file with echo -e, which also turned backslashes into escapes; printf.

runInstallFinishedCommands called sendResponse for each failed rotate
and again at the end, so one failure put several JSON bodies in the
reply. It now answers once, with the first error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestCancelKillsOnlyTheJobsXz killed only the bystander's bash, which
orphaned its yes | xz - every run left another pair spinning on the
machine running the tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n job (#153, #156)

A truncated image - an abandoned upload, a backup from before cancels
cleaned up - could only ever fail its integrity check, and nothing could
remove it. /api/delete_image takes a bare .img.xz name, refuses while
the drive is in use, and remounts ro afterwards; the UI offers Delete
next to the selected image, confirmed with a second click.

The Install/Backup button read "Install" and stayed enabled through a
download or upload, and a click sent cancel_installation or cancel_backup
at it. It is now disabled outside IDLE and its own operation, and only
sends a cancel for that operation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seen on A8 0498 while testing a wrong passphrase: iwd 3.8 segfaulted
during the restore (status=11/SEGV, with iwctl ap start-profile
running), systemd restarted it, and it came back in station mode - so
the board had no hotspot at all, which is the state this fallback exists
to prevent (#90). Once in five attempts; the same sequence run without
the new forget did not reproduce it either, so this is iwd, not the
caller.

Verify the mode after starting the AP, retry once, and say so plainly if
the hotspot still will not come up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)

Asking for the network the board is already on did nothing: iwd keeps
the association it has, the address never goes away, and the DHCP wait
took it as success straight away. A wrong passphrase therefore reported
"Success! Connected with IP" - the UI said Connected, and the server
saved credentials that had never worked, so they went into options.cfg
and into the next flashed image.

Seen on A8 0498, already on Kraakeslottet_5GHz: two retries with a wrong
passphrase, both logged as a success. With the disconnect, the same
attempt now fails after ~50s, restores the previous profile and falls
back to the hotspot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant