<feed xmlns='http://www.w3.org/2005/Atom'>
<title>tinyusb.git, branch claude/hil-blindness</title>
<subtitle>Unnamed repository; edit this file 'description' to name the repository.</subtitle>
<id>http://cgit.235523.xyz/tinyusb.git/atom/?h=claude%2Fhil-blindness</id>
<link rel='self' href='http://cgit.235523.xyz/tinyusb.git/atom/?h=claude%2Fhil-blindness'/>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/'/>
<updated>2026-08-27T10:04:37Z</updated>
<entry>
<title>test/hil, docs: move the containment history into the design doc</title>
<updated>2026-08-27T10:04:37Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T08:26:43Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=d014850098a9bb61c989cba608d190a9d50b3454'/>
<id>urn:sha1:d014850098a9bb61c989cba608d190a9d50b3454</id>
<content type='text'>
The modules were 21% comment, much of it review-cycle argument rather than
guidance -- _kill_kids stated 'descendant by construction, no argv check needed'
twice, eight lines apart. Deleting such comments outright makes maintenance
worse: the next reader simplifies the thing the comment was defending. So the
history moves to the 2026-07-30 fleet-wedge design doc, which gains a trim
addendum recording what was removed, what was deliberately kept, and the rule
that decided each -- the CI ceiling bounds how long a run burns, and does nothing
about state that outlives it.

One comment was not merely long but WRONG: the report wipe carried 'The unlink is
DEFERRED to inside the pool try/except below', which is the opposite of what the
code does -- it sits before Manager() with its own comment explaining why. That
is the failure mode this pass is about, so it is deleted rather than reworded.

Kept everywhere: citations that refute a plausible wrong reading. That
usb_lock_device_interruptible is why the readers are killable, that usblp_mutex
is driver-global, that rawmidi honours O_NONBLOCK where usblp does not.

Two follow-ups are retired with them: pr3803-hil-blindness-reporting.md (there
is no blindness to report any more) and pr3803-usbtest-recovery-reserve.md (the
reserve is derived now). Kept: pr3803-flasher-recover.md, which PR #3832
implements, plus pr3803-pci-rebind-stranding.md and pr3803-hil-iar-rerun-spec.md,
both independent of this work.
</content>
</entry>
<entry>
<title>test/hil: drop the sysfs blindness subsystem and derive the recovery reserve</title>
<updated>2026-08-27T10:04:37Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T08:26:43Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=3fad2d04f6f6b61ecd8d82576f69148172400614'/>
<id>urn:sha1:3fad2d04f6f6b61ecd8d82576f69148172400614</id>
<content type='text'>
Two layers whose cost was a contract to reason about rather than an outcome.

SYSFS_UNKNOWN was a three-valued return five files had to keep apart, and
misreading unknown as absence was silent: a healthy board reported as a firmware
regression. What it guarded is real -- `serial` is served by usb_string_attr,
which takes usb_lock_device_interruptible (v6.12.96 sysfs.c:141-143), the same
lock a wedged usbfs ioctl holds -- so the BOUND stays, on every caller by
default. usb_scan reads `serial` on every device matching the VID, and hil_lock's
controller_of does that on essentially every board, so one wedged DUT would
otherwise stall every worker, not one. What goes is the third value.

read_sysfs now returns str or None, and the question the third value existed to
answer is asked directly instead, by two predicates that say which question they
answer: sysfs_stranded() is process-wide and sticky, for hil_pool_check's footer
("could anything here be the tool losing sight of healthy hardware?"), and
path_stranded(path) is per-device, which is what usbtest needs to tell a DUT
whose `serial` is held under device_lock from one that genuinely left the bus --
that difference decides whether it performs driver-registry writes that take the
uninterruptible device_lock.

Gone: _SysfsUnknown, SYSFS_UNKNOWN, sysfs_blind, sysfs_blind_note,
note_sysfs_strand, the cross-process blindness publishing and its report banner,
usb_scan's (list, bool) return, usbtest's inconclusive abort, _blind_note's slot
in the result tuple, and bounded_open, whose last caller went in the previous
commit.

The strand memo is rewritten around the one invariant that makes it safe to
reuse: it is keyed by the path's kernfs inode, captured BEFORE the read. A
busport does not change when a board returns to the same physical port, so a
path-only blacklist outlives the wedge and hil_pool_check's own recovery flow --
reset, reflash, wait_device polling that busport -- would never look at the
board again. A re-enumeration destroys the kernfs node and makes a new one, so a
changed inode is the all-clear. Two ceilings bound different things: per path
(_PATH_STRAND_MAX) for a board that flaps while still wedged, and per process
(_STRAND_MAX) as a backstop against RLIMIT_NOFILE, counted per PATH rather than
per reader because hil_pool_check runs four poll threads over one bus.

A board the pool guard never reached is now reported as run-aborted rather than
pool-timed-out, and outranks a stale board-locked cell for the same reason the
pool-timeout cell does.

Both predicates answer conservatively where they are consulted before something
irreversible. path_stranded() covers the paths read_sysfs answered None for
WITHOUT reading -- past _STRAND_MAX it declines to start another reader, and
vouching for a path nobody looked at hands usbtest's fail-CLOSED guard a
fabricated all-clear, running remove_id/unbind against a wedged device. usbtest's
startup lookup carries the same caveat hil_test's absent arm already did, because
its stderr is relayed verbatim into the report cell.

USBTEST_RECOVERY_BUDGET was an independent 250s that could not actually contain
the ladder it reserved for, which is why usbtest.py carried a _time_left() gate
re-deciding before every step -- with a bare '- 35' for costs paid downstream
that nobody could re-derive. Between them the two produced a recovery that
skipped its own steps for most real hangs.

The reserve is now derived from the bounds usbtest itself declares, per flasher
and per target: a probe reset, a reflash, and the Rescue-DP POR plus retry a
wedged RP DAP needs, plus the settles and hil_util.REAP_GRACE for each bounded
step. The Rescue-DP legs are openocd-only and gated on the RP target cfg, and
a stub reset is screened out, so the reserve tracks each board's real ladder
instead of one fleet number: 390s for the two RP boards -- whose ladder the old
250 could not contain, which is exactly why the gates skipped their steps --
190s for the other seventeen probe-reset boards, and 150s for esptool and
lm4flash, whose reset is a no-op. Changing a bound in usbtest moves the
reserve with it, and a unit test asserts it covers the ladder.

With the room actually reserved, the child runs the ladder straight through:
recovery_steps, _time_left, the three per-step gates and the parsed-but-unused
--outer-timeout are gone. What stays is what decides outcomes -- the convoy_safe
gate, reset-before-reflash, the no_op screen so a stub that resets nothing is not
claimed, and wedged_pids() as the arbiter, because a clean flash only proves the
probe wrote the MCU.

hil_util.py 616 -&gt; 514 lines.
</content>
</entry>
<entry>
<title>test/hil: run the printer write in a child, like the read</title>
<updated>2026-08-27T08:26:43Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T08:26:43Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=bf2be7d352934b7bed93919f09a971cdb789b39e'/>
<id>urn:sha1:bf2be7d352934b7bed93919f09a971cdb789b39e</id>
<content type='text'>
test_device_printer_to_cdc opened /dev/usb/lp* on the worker itself and let
hil_util.bounded_open abandon a thread when the open blocked. usblp allows one
opener -- usblp_open() returns -EBUSY while usblp-&gt;used (v6.12.96 usblp.c) --
so the abandoned thread's fd poisoned the node for every later test that worker
ran. The read half already avoided this by forking; the write half now does too,
via the same run_alongside, and a killed child takes its fd with it.

This removes the only production caller of bounded_open.
</content>
</entry>
<entry>
<title>Merge pull request #3856 from hathach/claude/hil-drop-nt</title>
<updated>2026-08-27T08:09:49Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T08:09:49Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=42db982e7c6e8ee7d2f80d15a93a7092d3b708c9'/>
<id>urn:sha1:42db982e7c6e8ee7d2f80d15a93a7092d3b708c9</id>
<content type='text'>
test/hil: drop the Windows accommodations, which accommodate nothing</content>
</entry>
<entry>
<title>Merge pull request #3855 from hathach/claude/pr-babysit-split-triage</title>
<updated>2026-08-27T07:56:23Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T07:56:23Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=677182694f555c451eaec0a0d4ee7b14ac532783'/>
<id>urn:sha1:677182694f555c451eaec0a0d4ee7b14ac532783</id>
<content type='text'>
workflows/agents: overlap pr-babysit's review and CI lanes; split pr-monitor; pin agent efforts</content>
</entry>
<entry>
<title>validate: add claude + codex diff-review stages (opus/high, sol/high)</title>
<updated>2026-08-27T05:21:04Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T05:21:04Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=92b38fc3b5e9080c96294079e6117d74429f3448'/>
<id>urn:sha1:92b38fc3b5e9080c96294079e6117d74429f3448</id>
<content type='text'>
The claude stage reviews the diff directly (the code-review skill is a CLI
built-in, unavailable to subagents); the gate is enforced in-script from
structured findings, failing only on confirmed correctness/safety bugs.
</content>
</entry>
<entry>
<title>pr-babysit: overlap a fast review lane with the CI watch</title>
<updated>2026-08-27T05:21:04Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T05:21:04Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=208f82efe8b9dc7ca3eb210dcbf455a14c97cf3c'/>
<id>urn:sha1:208f82efe8b9dc7ca3eb210dcbf455a14c97cf3c</id>
<content type='text'>
Review findings are validated, fixed, and pushed without waiting on CI;
checkoutDir decouples the PR checkout from the session cwd. File-less CI
failures are scoped by a dedicated agent, paths canonicalized and
existence-checked via git ls-files, overlapping groups merged. Per-id
reply/resolve accounting retries failures and holds the green exit until
all outward work is drained.
</content>
</entry>
<entry>
<title>agents: split pr-monitor into pr-ci-watcher + pr-review-validator; rename port-dev/driver-reviewer to code-writer/code-verifier; pin model+effort on every agent</title>
<updated>2026-08-27T05:21:04Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T05:21:04Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=2b9ddd8b00eb57225fd9caeecd0da1a7954aee06'/>
<id>urn:sha1:2b9ddd8b00eb57225fd9caeecd0da1a7954aee06</id>
<content type='text'>
</content>
</entry>
<entry>
<title>test/hil: drop the Windows accommodations, which accommodate nothing</title>
<updated>2026-08-27T01:54:07Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-26T18:01:30Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=9baa97a8c6cc671aefe26168b3d8281da070ebf9'/>
<id>urn:sha1:9baa97a8c6cc671aefe26168b3d8281da070ebf9</id>
<content type='text'>
hil_test.py cannot run on Windows and never could: it imports helper.hil_lock,
whose module-level `import fcntl` is POSIX-only, so the harness fails at import
before a line of it executes. Past that it reads /sys/bus/usb, /dev/bus/usb,
/dev/serial/by-id and /proc, kills by process group, and takes flock board
locks -- none of which Windows has.

So the guards were protecting a platform the code cannot reach:

- run_cmd branched three ways on os.name to decide whether to set
  start_new_session and whether to killpg. The non-POSIX arm called p.kill()
  instead, which kills only the direct child -- exactly the semantics the whole
  containment design rejects, since a flasher run through a shell reparents out
  of reach. Dead code that documented the wrong answer.
- hil_test picked multiprocessing's default context on Windows "so it still
  IMPORTS there". It does not import there.
- test_device_audio_test_freertos returned 'skipped' on nt before touching
  ALSA, in a function only ever reached from a worker that cannot start there.
- Seven @unittest.skipIf(os.name == 'nt') decorators across the two suites.
  These were the only ones with a real effect -- the unit tests DO import and
  run on Windows, because they stub pyserial and mostly exercise pure logic --
  but what they buy is a partially-green suite for a harness that cannot run,
  and nothing verifies the set is correct: the hil-test hook only ever runs on
  ubuntu-latest, so a missing guard fails silently until someone tries.

Removing them makes the POSIX assumption single and explicit rather than
scattered and half-honoured. Nothing changes on Linux: every removed branch was
the one already taken there.

Removing the run_cmd guards also removes their `else: p.kill()` arms. Those were
the Windows branches, and p.kill() reaches only the direct child -- a flasher run
through a shell keeps grandchildren it cannot touch, which is the semantics this
containment design rejects. RunCmdCleanupShape pins what is left: both cleanup
paths killpg, no try carries an else whose body would run when the kill
SUCCEEDED, and the BaseException path still re-raises. Structural rather than
behavioural because driving a real SIGINT into a blocked communicate() is
timing-dependent, and what actually breaks this block is an edit that rebinds a
branch -- which is a shape.
</content>
</entry>
<entry>
<title>test/hil: run the HID echo in a child, which is the only bound that works (#3852)</title>
<updated>2026-08-27T01:43:02Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T01:43:02Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=d799b6f572e4b39ebebcf72126d3120f7829c034'/>
<id>urn:sha1:d799b6f572e4b39ebebcf72126d3120f7829c034</id>
<content type='text'>
hid_generic_inout was the last unbounded blocking IO in the file. hidapi's
hidraw backend reads manufacturer/product via udev for each device reaching
create_device_info_for_device, both usb_string_attr served under the device
lock a wedged usbfs ioctl holds — and every DUT here is VID cafe, so a wedged
sibling stalls the walk.

A thread cannot bound it: cython-hidapi calls hid_open and hid_close bare
(0.15.0 hid.pyx), so they hold the GIL and the waiter can never resume.
Measured — a 1.0s bound never returned. run_cmd's killpg reaches a child
regardless; it gains an argv form for the -c body.

Filters on both ids: hidapi only runs the free uevent pre-check when ids are
passed (linux/hid.c:962), so an unfiltered walk sends every device straight to
the locked reads. Tests stall via ctypes.PyDLL, which unlike CDLL holds the
GIL — the shape a thread bound cannot cover.</content>
</entry>
</feed>
