Reboot into an installed image whatever the bus says #55
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/install-reboot-cancelled"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
CCS-UF-13.
Node::loopdrains inbound frames beforeWifiManager::loop, so the reboot into an installed image hung on session bookkeeping that aSYS_WIFI_CONTROLframe clears.No board can reach that window today:
FirmwareUpdateronly reachesInstalledinside its ownloop(), and the old check followed in the same call, so nothing could get between them. The reboot was correct by accident —WifiUpdaterpromises nothing about wheninstalled()turns true, and any early return added in between reopens it silently, in the one path that ends with a node running the old image and the new slot armed. So this is hardening, not a live bug.Once the boot slot is switched,
onFrameignores control frames andrebootIfInstalled()resets, asking the updater and a one-shot latch alone. The ticket'sif (forUpdate)gate would close action 1 only: 0 and 2 clear the same bookkeeping throughendSession(), 3 abandons the installed image in a freshbeginUpdate, and a dropped link reaches the window too.Six new cases in
test_wifi_manager. Five fail against base; the sixth pins the reset to the loop the install happened in. Spec section 11 gains one sentence.The fix itself holds up: all five new tests fail against the base code, the whole native suite passes, and clang-format is clean. Four things to act on, plus one on the description.
The description is three long paragraphs of reasoning that already lives in the code comments. A couple of lines — the window, and that the latch asks the updater alone — is the convention here.
@ -108,2 +100,2 @@boot_->restart();return;// Again here, so an install is acted on in the loop it happened in.if (rebootIfInstalled()) return;Nothing covers this call site: delete it and all 59 tests still pass. Every test sets
installedImagebeforenode.loop, so the top-of-loop check is what fires. Without it the reboot slips one loop and, if the idle timeout has expired in the same loop,disable()puts a state 0 on the wire first — which is the thingtest_an_installed_image_takes_the_radio_off_and_rebootsasserts against. Either add aninstallOnLooptoFakeUpdater, mirroringfailOnLoop, and a test that the install happening insideupdater_->loop()reboots in that loop, or drop the second call.@ -117,0 +111,4 @@// Once only, since an installed image stays installed. No status frame on the way// out; the build identifier in the next announce is what says the update took.bool WifiManager::rebootIfInstalled() {if (rebootRequested_ || boot_ == nullptr || updater_ == nullptr || !updater_->installed()) return false;This now rests on an undocumented
WifiUpdatercontract:installed()must survivecancel(). A control frame arriving in the window reachesendSession()→cancel()before this check runs, and onlyFirmwareUpdater::cancel()'sif (inProgress())guard keeps the stage atInstalled. An updater that reset its stage incancel()would silently reopen the exact bug. Say so onWifiUpdater::installed()in the header.@ -239,1 +239,4 @@// Joined, with the image installed and the manager not yet round to seeing it:// the window an inbound frame lands in, since Node::loop drains frames first.struct InstalledSession {test_a_session_after_an_install_is_not_rebooted_againisInstalledSessionplusnode.loop(t0 + 2000)— twelve lines of the same setup left next to the new helper. Use the helper there too.All four findings acted on in
c6e7759.FakeUpdatergainedinstallOnLoopand a test for an install inside the updater's own loop, asserting the reboot happens in that loop and sends no status. Removing the secondrebootIfInstalled()call fails it.WifiUpdater::installed()says it stays true throughcancel().UpdatingSessionwith aninstall(), used by all the install tests including the two that predate this branch.On the residual you noted: a second install in one process run would not reboot, since the latch never clears. It needs
restart()to return, whichesp_restart()does not, so I have left it rather than adding state for a case no board can reach.Three things, none in the reboot path itself — that part holds up under mutation.
The description says "Six cases in
test_wifi_manager, each of which fails against the code before the fix." Five do.test_an_image_installed_inside_the_updaters_loop_reboots_in_that_looppasses onfeat/firmware-updates: the old block sat after the updater's loop too, so it already rebooted in the install's own loop. It only fails if the secondrebootIfInstalled()call is dropped, which makes it a regression guard for this branch's own refactor — worth keeping, worth describing accurately.An action 2 frame arriving in the install window is still acted on before
loop()gets torebootIfInstalled():startAp()brings the AP up and broadcastsSYS_WIFI_STATUSstate 3, then the radio goes off and the node resets. Verified — the last status on the bus before the reset says the AP is up, and that AP never exists. It sits badly against "No status frame on the way out" inrebootIfInstalled()and against thestatusCount() == 0assertion intest_an_installed_image_takes_the_radio_off_and_reboots.Cheapest fix is an early return in
onFrameonceboot_ != nullptr && updater_ != nullptr && updater_->installed()— the reboot is not the session's to call off, so the frame has nothing left to change. Action 0's state 0 is honest enough; action 1 and 3 report nothing here.@ -1020,0 +1048,4 @@node.loop(t0 + 3000);TEST_ASSERT_EQUAL_UINT8(1, session.boot.restartCalls);TEST_ASSERT_EQUAL_UINT8(0, statusCount());This assertion cannot fail. At
t0 + 3000the 5 s report interval has not elapsed and the state is unchanged, sostatusCount()is 0 whether or not the reboot happens in this loop — with the secondrebootIfInstalled()call removed, only line 1050 fails. Drop it and theclearBus()above it.The comment above the test has the same problem: no status goes out in this loop either way, and if the loop did land on the report boundary the report would fire before
updater_->loop()and so be unaffected by the fix. Reword it to what the test does pin — the reboot lands in the install's loop, not the one after.The window this branch defends does not exist with the real updater, so on a node this changes no behaviour. Detail inline, plus the spec wording, the comment density and one weak fixture.
@ -959,3 +959,3 @@a transfer short: one already running reaches completion or failure first, so ashort timeout cannot drop the radio mid-image. A transfer that stalls, or thatcannot finish within five minutes, fails of its own accord.cannot finish within five minutes, fails of its own accord. Once the boot slotIf the guard stays, this covers the reboot but not the frames. A
SYS_WIFI_CONTROLin that window is dropped: no status, and nothing services it after the reboot, so an action 2 asking for an AP simply disappears. Say that, e.g. "ASYS_WIFI_CONTROLframe that arrives after the slot is switched is ignored, with no status frame."@ -30,1 +30,4 @@// Nothing left of a session whose boot slot is switched but the reboot, so a// frame acted on here would only report a state the reset then interrupts.if (rebootDue()) return;This guard is unreachable.
FirmwareUpdateronly reachesStage::Installedinside its ownloop()(finishImageviapump),WifiManager::loopruns the install check in the same call immediately after (line 105, and the same position in the base), andArduinoBootControl::restart()isesp_restart(), which does not return. So no frame drain ever runs withinstalled()true andrebootRequested_false, andonFramecan never seerebootDue().The five cases that fail against base fail only because
FakeUpdaterlets a test setinstalledImagefrom outsideloop()— a state the real updater cannot produce. Either drop the guard with its four window tests, or keep it as declared hardening and stop the description claiming a frame "could cancel the reboot"; as it stands the branch is a refactor plus tests for a fabricated state, not a fix for a live bug.@ -107,3 +104,1 @@state_ = can::wifi_state::off;boot_->restart();return;// Again here, so an install is acted on in the loop it happened in.The ordering point is made three times — line 64, here, and lines 113-114 — for six statements of code. Keep it once, on
rebootIfInstalled(), and drop the other two.@ -93,6 +96,7 @@ class WifiManager {bool updateRequested_ = false;bool updateStarted_ = false;bool updateFailed_ = false;bool rebootRequested_ = false;The latch never clears, so on a platform whose
restart()returns, a second install in the same power cycle never reaches step 7; the base'supdateStarted_gate did not have that. Academic whileesp_restart()never returns — flagging in case you would rather clear it when a new update starts.@ -240,0 +266,4 @@// Installed, with the manager not yet round to seeing it: the window an inbound// frame lands in, since Node::loop drains frames before WifiManager::loop.void install() {rebootDue()no longer looks at session bookkeeping, so the four window tests pass even if the fixture's join or fetch silently never happened. Pin the premise with one assert in the constructor,TEST_ASSERT_EQUAL_UINT8(1, updater.calls).Round three,
82699f3.FirmwareUpdaterflips toInstalledinside the call the check follows.SYS_WIFI_CONTROLand the absent status frame are both in it now.rebootDue()and theonFrameguard each carry their own fact instead of repeating it.UpdatingSessionnow assertsupdater.calls == 1, so the window tests pin their premise.restart()to return.Fit to merge.
One slip: the description says "Seven cases in
test_wifi_manager. Five fail against base; the other two". Six were added (54 to 60RUN_TESTs); five fail against base and one —test_an_image_installed_inside_the_updaters_loop_reboots_in_that_loop— pins the secondrebootIfInstalled()call. Nothing new pins the absent status frame; that assertion predates the branch.