Stop a firmware download when the service is stopping #63
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/stop-a-download-on-shutdown"
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?
A fetch runs in a thread — the poll's, or the one the control API answers on — and
serveonly cancels the coroutine waiting on it. The thread carried on reading, so a container asked to stop sat there until the image finished arriving or Docker killed it. On a Pi losing power that is a shutdown cut off part way through a write.firmware.FETCH_STOPis what the thread watches.ForgejoReleases.downloadnow reads a chunk at a time instead ofcopyfileobjand gives up between chunks, and_checkstops before opening a connection for the next asset. An interrupted fetch raisesFetchStoppedError, whichstorealready cleans up after: the.partgoes, noimage.jsonis written, and the next check fetches the image again.refreshandrefresh_nowreleaseREFRESH_LOCKon that path as on any other.Measured with a 20 MB image trickling in over 16 seconds: stopping took 15.8 s before and 0.049 s after — one chunk. A read that hangs outright is still bounded by
REQUEST_TIMEOUTrather than by this.Tested with six tests that all fail without the change: the fetch giving up between chunks, a check that begins while stopping fetching nothing, the cache left with no usable entry, a later check fetching the image again, the lock free afterwards, and a stop while a fetch is in flight getting the thread back inside a second. Each drives the real
ForgejoReleasesthrough a fake opener that hands the image out in small pieces, so none of them depends on scheduling luck. A conftest fixture gives every test its ownFETCH_STOP.CCS-UHA-11.
Three points, none of them blocking the mechanism itself — the stop reaches the thread and the cache is left clean.
@ -514,0 +686,4 @@def crawl() -> None:started.set()time.sleep(CRAWL)Only test here that turns on elapsed time: it proves the stop by finishing inside 1s where the full crawl needs 6s. It cannot pass falsely, but a stalled box flakes it to fail. If you want it exact, have
betweenblock on athreading.Eventthe test releases a chunk at a time instead of sleepingCRAWL.A stopped download is reported as a fetch failure.
storereturnsNoneonFetchStoppedError, so the asset lands inCheck.failed, andbutton.pyturns that intoHomeAssistantError("The check could not fetch node_lighting-....bin"). Verified: a check stopped mid-download returnsreached=True, failed=('node_lighting-2026.09.4-firmware.bin',). Skip the append whenFETCH_STOP.is_set(), and assert onfailedin one of the new tests — nothing covers it today.@ -38,2 +38,4 @@"""settings.firmware_cache.mkdir(parents=True, exist_ok=True)# Cleared on the way in so a stop cannot outlive the run that asked for it.firmware.FETCH_STOP.clear()clear()here is unreachable in production and untested.serveis called once per process (main->run->serve), and the full suite passes with this line deleted because the conftest fixture gives each test its own event. Drop it, or keep it with a test for the in-process restart it is guarding.All three taken, nothing further. The handshake test now passes in 10ms with no clock in it, and still fails when the chunk check or
serve'sFETCH_STOP.set()is removed. The merge of the base is clean: the three-dot diff is still only conftest, test_firmware_cache, firmware.py and service.py.