tool: survive our own leftovers — drain the fresh port, shorten the identity read
--stay leaves the loader's final prompt in the USB pipeline; a fresh invocation on a board that resets when its port opens then flushes too early, trusts the stale prompt, and spends the new activation window on a 2-second identity read against a device that never heard its knock — collecting the application's banner as an unknown signature. Three host-side moves, no device bytes: the line is drained until quiet (bounded, 250 ms) before the port's first knock — once per port, since a mid-session re-knock faces no foreign bytes and its own window is already burning; the identity read_exact drops 2.0 to 0.5 s, dozens of times the worst real answer, so any false prompt match leaves room for the retry that already works; and the tool version drifts to 8. The StaleDTRPort fixture models the whole moment — stale prompt in transit, reset holding the device off the line, a finite window, the banner — red against the old tool in exactly the field shape (unknown signature from banner bytes), green now; LoaderPort answers its prompt to the knock rather than to a read count, which the drain exposed as a call-order coupling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ else:
|
||||
import termios
|
||||
|
||||
PROMPT = b"+"
|
||||
VERSION = 7 # this tool's own version — free to drift from a loader's
|
||||
VERSION = 8 # this tool's own version — free to drift from a loader's
|
||||
# The loader versions this tool can drive. A pureboot version implies its wire
|
||||
# protocol, which carries no number of its own, so this window is where that
|
||||
# map lives: the tool keeps a decoder for every generation in it (1–4 speak
|
||||
@@ -546,6 +546,11 @@ class Loader:
|
||||
# Set once a session is established over an autobaud link, so a
|
||||
# re-entry after 'J' repeats the handshake that worked.
|
||||
self.autobaud = False
|
||||
# The pre-knock drain runs once per port: the bytes it exists for are
|
||||
# leftovers from before this process opened the port. Re-knocks later
|
||||
# in the same session must not pay it — a fresh activation window is
|
||||
# already burning while they wait.
|
||||
self._line_drained = False
|
||||
# The link this session is speaking. It moves when the host follows a
|
||||
# staging copy built for another one (enter_copy).
|
||||
self.baud = getattr(port, "baud", None)
|
||||
@@ -555,10 +560,16 @@ class Loader:
|
||||
"""The 'b' reply, in either of the two layouts a loader may send.
|
||||
pureboot 5 answers with its version and the signature; older loaders
|
||||
answer with a 12-byte block. The version byte cannot be mistaken for
|
||||
the older block's 'P', so four bytes are enough to tell them apart."""
|
||||
head = self.port.read_exact(4, 2.0)
|
||||
the older block's 'P', so four bytes are enough to tell them apart.
|
||||
|
||||
The timeout is short on purpose: a real answer follows the prompt
|
||||
within a frame time or two, so half a second is dozens of times the
|
||||
worst case — while a *false* prompt match (a stale byte, reset
|
||||
garbage) makes this read collect noise, and every second spent on it
|
||||
comes out of the activation window the retry needs."""
|
||||
head = self.port.read_exact(4, 0.5)
|
||||
if head[0:2] == b"PB":
|
||||
return Info(head + self.port.read_exact(8, 2.0))
|
||||
return Info(head + self.port.read_exact(8, 0.5))
|
||||
return Info.from_identity(head)
|
||||
|
||||
def _handshake(self, wait, knock, what):
|
||||
@@ -569,8 +580,23 @@ class Loader:
|
||||
into a fresh window, where a command without its knock is discarded.
|
||||
Each attempt is therefore the whole handshake. This also converges into
|
||||
an already-live session: the knock bytes are ignored there and the
|
||||
drain absorbs whatever they produced."""
|
||||
drain absorbs whatever they produced.
|
||||
|
||||
Before the port's first knock ever, the line is drained until quiet: a
|
||||
prompt from a previous session (`--stay`) can still be in the USB
|
||||
pipeline when the port opens, where a flush cannot clear what has not
|
||||
arrived yet — and on a board that resets when its port opens, trusting
|
||||
that stale byte would spend the fresh activation window reading noise
|
||||
from a device that never heard the knock. Once only, and bounded:
|
||||
later re-knocks in this session face no foreign leftovers, and their
|
||||
own window is already burning."""
|
||||
deadline = time.monotonic() + wait
|
||||
if not self._line_drained:
|
||||
self._line_drained = True
|
||||
drain = time.monotonic() + 0.25
|
||||
while self.port.read_available(0.05):
|
||||
if time.monotonic() > drain:
|
||||
break
|
||||
knocks = 0
|
||||
refusal = None
|
||||
while True:
|
||||
|
||||
Reference in New Issue
Block a user