Skip to content

windows: take the adapter names netsh accepts, and find a built agent - #135

Merged
openipc-ai merged 3 commits into
masterfrom
windows-tftp-and-agent-lookup
Sep 8, 2026
Merged

windows: take the adapter names netsh accepts, and find a built agent#135
openipc-ai merged 3 commits into
masterfrom
windows-tftp-and-agent-lookup

Conversation

@openipc-ai

@openipc-ai openipc-ai commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Four faults, all found by one reporter recovering a bricked Hi3516CV300 on Windows in OpenIPC/firmware#2381. They got defib burn working, reached Phase 2: Flash via TFTP, and stopped there.

1. The adapter name was one netsh cannot accept

list_interfaces() took its names from socket.if_nameindex(), which answers on Windows with ethernet_0. netsh interface ip add address addresses adapters by their friendly name and has never heard of that, so it failed with:

IPManagerError: Failed to add IP 192.168.1.10 to ethernet_0: Failed to configure
the DHCP service. The interface may be disconnected.

which sends the reader after a cable. The reporter got past it by renaming their Windows adapter to ethernet_0 to match.

Names now come from netsh interface show interface, parsed by column position rather than header text because netsh is localised. Names with spaces (Ethernet 2 is a stock Windows name) survive.

2. An address that was already up was fatal

netsh answers "The object already exists", add_ip raised, so setting the IP up by hand beforehand did not help either. Bindability now decides, before and after the command — it is the property the caller actually wants (the TFTP server binds this address), and unlike command output it is not translated.

add_ip returns whether it assigned the address, and temporary_ip removes it only in that case. Taking away the operator's own static IP on the way out is not ours to do.

3. The bind raced the address coming up

netsh returns before the stack can bind the address, so the TFTP bind that follows immediately failed with EADDRNOTAVAIL. That unwound the AsyncExitStack, which removed the address — so the log read as though defib had taken away what it had just assigned:

INFO:defib.network.ip_manager:Successfully added 192.168.1.10 to ethernet_0
  IP assigned
INFO:defib.network.ip_manager:Removing IP 192.168.1.10 from ethernet_0

The reporter's reading — "Why is it REMOVING the ip it just added before trying to bind TFTPd to it?" — was right about the sequence and the cleanup is the symptom. add_ip now polls until the address is bindable before returning.

4. defib agent could never work from an installed defib

get_agent_binary searched exactly one path: the git checkout four levels above the module. In an installed package that resolves inside site-packages and can never exist, so the whole defib agent family was unreachable for anyone who installed defib the documented way. The refusal —

No agent binary for 'hi3516cv300'

— also reads as an unsupported chip, on a chip that is supported. It now searches DEFIB_AGENT_DIR, a packaged binaries/ directory, the cache directory and the checkout; tries both the chip name and the mapped build name, so a make SOC=gk7205v300 output is found even though that chip shares the gk7205v200 map; and when it comes up empty says what to build and how.

While there: hi3516ev200 has had its own stanza in agent/Makefile all along but was missing from the chip map, so defib agent refused a chip the agent builds. A test now reads the SOC list out of the Makefile and fails if the map falls behind it again.

Testing

Two new test files, 27 tests, plus the full suite:

771 passed, 2 skipped in 54.46s

I have no Windows machine of my own, but the CI matrix runs windows-latest — so the one claim the interface fix rests on, that netsh can be asked for names netsh will accept, is checked against the real command there. The rest of the Windows paths are driven by tests that feed the netsh table and the command results directly. The end-to-end flash on real hardware is still unverified; the reporter is mid-recovery and willing to retry.

OpenIPC/firmware#2381 recovered a bricked Hi3516CV300 as far as "Phase 2:
Flash via TFTP" and stopped there, on four separate faults.

`list_interfaces()` took its names from `socket.if_nameindex()`, which
answers on Windows with "ethernet_0". netsh addresses adapters by their
friendly name, has never heard of that, and says so as "Failed to configure
the DHCP service. The interface may be disconnected." -- which sends the
reader after a cable. The reporter got past it by renaming their adapter to
`ethernet_0`. Ask netsh for the list instead, parsed by column position
because netsh is localised, so the names we hand out are names we can hand
back.

`add_ip` treated an address that was already up as fatal, so setting it up
by hand first did not help either. Bindability now decides, before and after
the command: it is the property the caller actually wants, and unlike
command output it is not translated. An address we did not add is no longer
removed on the way out -- taking away the operator's own static IP is not
ours to do.

netsh also returns before the stack can bind the address, so the TFTP bind
raced it and lost with EADDRNOTAVAIL. The failure unwound the context
manager, which removed the address, so the log showed defib taking away what
it had just assigned. `add_ip` now waits for the address to come up.

Separately, `get_agent_binary` searched exactly one path: the git checkout
four levels above the module. In an installed package that resolves inside
site-packages and can never exist, so the whole `defib agent` family was
unreachable for anyone who installed defib the documented way -- and the
refusal, "No agent binary for 'hi3516cv300'", reads as an unsupported chip
on a chip that is supported. It now searches DEFIB_AGENT_DIR, a packaged
binaries/ directory, the cache directory and the checkout, tries both the
chip name and the mapped build name so a `make SOC=gk7205v300` output is
found, and when it comes up empty says what to build and how.

hi3516ev200 has had its own stanza in agent/Makefile all along but was
missing from the chip map, so `defib agent` refused a chip the agent builds.
A test now reads the SOC list out of the Makefile and fails if the map falls
behind it again.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix Windows temporary IP setup and installed agent discovery

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Uses netsh-friendly adapter names and waits for temporary IPs to become bindable.
• Preserves pre-existing addresses and searches install-safe locations for compiled flash agents.
• Adds actionable diagnostics, documentation, and regression coverage for both recovery paths.
Diagram

graph TD
  UI["CLI and TUI"] --> LOOKUP["Agent lookup"] --> PATHS[("Search locations")]
  RECOVERY["Recovery flow"] --> IP["IP manager"] --> NETSH["Windows netsh"]
  IP --> BIND["Bindability gate"] --> TFTP["TFTP server"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Bundle prebuilt agent binaries
  • ➕ Makes installed agent commands work without a compiler or manual binary placement.
  • ➕ Provides deterministic binaries tested and versioned with each release.
  • ➖ Requires a trusted cross-compilation and release pipeline for every supported SoC.
  • ➖ Increases package size and ongoing artifact maintenance.
2. Use PowerShell adapter APIs
  • ➕ Can return structured adapter data instead of parsing netsh tabular output.
  • ➕ Provides direct access to Windows friendly names and adapter state.
  • ➖ Adds PowerShell invocation, policy, version, and serialization dependencies.
  • ➖ Splits enumeration from the existing netsh-based address mutation path.

Recommendation: The PR's approach is the best low-risk fix: it keeps the existing netsh integration, validates the actual property TFTP requires, tracks address ownership, and makes locally built agents discoverable without changing distribution policy. Prebuilt agents could improve installation UX later, but should be introduced through a separately reviewed artifact and release pipeline.

Files changed (7) +602 / -42

Bug fix (4) +267 / -42
client.pyResolve agent binaries from install-safe locations +99/-23

Resolve agent binaries from install-safe locations

• Centralizes chip-to-agent mappings, adds hi3516ev200 support, and searches environment, package, cache, and checkout directories using both chip and mapped build names. Adds actionable diagnostics for unsupported chips and missing compiled binaries.

src/defib/agent/client.py

app.pyExpose actionable agent lookup failures in CLI commands +14/-4

Expose actionable agent lookup failures in CLI commands

• Replaces ambiguous missing-binary errors in agent upload and flash commands with guidance explaining support, build commands, searched paths, and TFTP alternatives.

src/defib/cli/app.py

ip_manager.pyMake Windows temporary IP configuration reliable +150/-13

Make Windows temporary IP configuration reliable

• Lists Windows adapters using netsh-friendly names, treats already-bindable addresses as success, and waits for newly assigned addresses to become usable. Temporary cleanup now removes only addresses created by the current run and reports more relevant interface diagnostics.

src/defib/network/ip_manager.py

flash_doctor.pyShow detailed agent binary guidance in Flash Doctor +4/-2

Show detailed agent binary guidance in Flash Doctor

• Uses the shared agent lookup help text when Flash Doctor cannot locate a compiled binary.

src/defib/tui/screens/flash_doctor.py

Tests (2) +317 / -0
test_agent_binary_lookup.pyCover agent mapping, discovery, and diagnostics +109/-0

Cover agent mapping, discovery, and diagnostics

• Tests environment overrides, mapped and chip-named binaries, variant handling, absolute search paths, user guidance, and consistency between the Makefile SOC list and supported chip mappings.

tests/test_agent_binary_lookup.py

test_ip_manager_windows.pyCover Windows adapter and temporary IP regressions +208/-0

Cover Windows adapter and temporary IP regressions

• Tests localized netsh parsing, friendly names with spaces, pre-existing addresses, delayed bindability, failure guidance, and ownership-aware cleanup behavior.

tests/test_ip_manager_windows.py

Documentation (1) +18 / -0
README.mdDocument compiled agent binary discovery +18/-0

Document compiled agent binary discovery

• Documents the ordered agent search locations, chip-versus-build filenames, and setup options for installed packages that do not ship prebuilt binaries.

agent/README.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Windows recovery stops responding ✓ Resolved 📘 Rule violation ➹ Performance
Description
_windows_interfaces() runs subprocess.run() synchronously with a ten-second timeout instead of
using an asyncio subprocess or offloading the work. On Windows, async network recovery and
installation call list_interfaces() directly, so adapter discovery or its failure advice blocks
every task on the event loop until netsh exits.
Code

src/defib/network/ip_manager.py[R247-250]

+        proc = subprocess.run(
+            ["netsh", "interface", "show", "interface"],
+            capture_output=True, text=True, timeout=10,
+        )
Evidence
Compliance rule 15 prohibits blocking behavior in asyncio execution paths without isolation. The
added synchronous netsh invocation is reached directly from async CLI workflows through
list_interfaces(), and the configured timeout permits the event loop to remain blocked for up to
ten seconds.

CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations: CLAUDE.md: Use Asyncio for Asynchronous Python Operations
src/defib/network/ip_manager.py[247-250]
src/defib/network/ip_manager.py[283-285]
src/defib/cli/app.py[852-855]
src/defib/cli/app.py[2600-2605]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Windows adapter discovery invokes synchronous `subprocess.run()` from asyncio command paths, potentially blocking the event loop for ten seconds.
## Issue Context
Both normal interface selection and failed IP-assignment advice can reach `_windows_interfaces()`. Use an asyncio subprocess API or explicitly isolate synchronous discovery in a worker thread while preserving existing timeout and error handling.
## Fix Focus Areas
- src/defib/network/ip_manager.py[144-152]
- src/defib/network/ip_manager.py[247-250]
- src/defib/cli/app.py[852-855]
- src/defib/cli/app.py[2600-2605]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Failed setup leaves a host IP behind ✓ Resolved 🐞 Bug ☼ Reliability
Description
add_ip raises after a successful assignment when _wait_until_bindable times out, before
temporary_ip enters the try/finally that owns removal. A delayed address, cancellation, or
timeout therefore leaves the temporary address configured and can make later recovery runs
misclassify it as operator-owned.
Code

src/defib/network/ip_manager.py[R134-139]

+    if not await _wait_until_bindable(ip):
+        raise IPManagerError(
+            f"{ip} was assigned to {interface} but never became usable. "
+            f"Check that the adapter is connected and that {ip} does not "
+            f"collide with an address already on this host."
     )
Evidence
The platform command completes before the new timeout path raises, while the context manager
establishes its cleanup only after add_ip returns. Both TFTP workflows enter this context before
binding their server, so the leaked assignment affects failed recovery attempts.

src/defib/network/ip_manager.py[119-141]
src/defib/network/ip_manager.py[184-213]
src/defib/cli/app.py[921-924]
src/defib/cli/app.py[2646-2657]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A successful address assignment is not removed if the subsequent bindability wait times out or is cancelled. Ensure that once the platform command succeeds, every failure before ownership reaches `temporary_ip` removes the address that this run added.
## Issue Context
`temporary_ip` cannot run its cleanup because `add_ip` raises before returning its ownership flag. Preserve the original exception while making cleanup best-effort and avoid removing addresses that were already present.
## Fix Focus Areas
- src/defib/network/ip_manager.py[119-141]
- src/defib/network/ip_manager.py[184-213]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Valid local agents can become unusable ✓ Resolved 🐞 Bug ☼ Reliability
Description
_agent_search_path eagerly calls get_agent_cache_dir, whose firmware helper creates the cache
directory before any candidate is examined. If that cache location is unwritable, lookup raises even
when a valid binary exists in DEFIB_AGENT_DIR, the installed package, or the checkout.
Code

src/defib/agent/client.py[R217-222]

+    # Shipped inside the package, for a wheel that carries prebuilt agents.
+    directories.append(Path(__file__).parent / "binaries")
+    # Built by hand and dropped in the cache next to downloaded firmware.
+    directories.append(get_agent_cache_dir())
+    # A git checkout with `make SOC=<soc>` already run in agent/.
+    directories.append(Path(__file__).parent.parent.parent.parent / "agent")
Evidence
The newly added search-path construction always evaluates get_agent_cache_dir; that delegates to
get_cache_dir, which unconditionally calls mkdir. Because candidate iteration starts only after
the complete list is returned, a filesystem error blocks earlier and later valid locations alike.

src/defib/agent/client.py[179-181]
src/defib/agent/client.py[213-224]
src/defib/agent/client.py[227-231]
src/defib/firmware.py[71-81]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Agent lookup must not fail merely because the optional cache directory cannot be created. Avoid eagerly creating the cache while assembling search candidates, or safely skip an inaccessible cache candidate.
## Issue Context
Search-path construction happens before `get_agent_binary` checks its first candidate, so even the environment override currently depends on an unrelated writable cache location. Directory creation should occur only when writing a binary, not while reading or formatting help.
## Fix Focus Areas
- src/defib/agent/client.py[194-231]
- src/defib/firmware.py[71-81]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/defib/network/ip_manager.py
Comment thread src/defib/network/ip_manager.py Outdated
Comment thread src/defib/agent/client.py
Ruff caught the `sys` import the mocked tests did not need, since they
patch `ip_manager.sys` rather than their own.

Putting it back for a skipif: the CI matrix runs on windows-latest, so the
one claim the interface fix rests on -- that netsh can be asked for names
netsh will accept -- can be checked against the real command there instead
of only against a captured table.
Three findings from the review of this branch, all of them mine.

`_windows_interfaces()` ran `subprocess.run` with a ten-second timeout, and
`list_interfaces()` is reached from the async install and network paths and
from add_ip's own failure advice. Blocking the loop there stops every other
task, including the serial link to a camera sitting in its bootrom window,
which does not wait for us. The netsh table parser is now separate from the
call that produces it, so `list_interfaces_async()` can reuse it over
`asyncio.create_subprocess_exec` while the synchronous `list-interfaces`
command keeps the synchronous one.

`add_ip` raised when the bindability wait timed out, having already assigned
the address -- and `temporary_ip` cannot clean up, because it does not learn
that we own the address until add_ip returns. So a delayed address left the
host reconfigured, and the next run read it as the operator's and left it
alone for good. Every exit after a successful assignment now takes the
address back down, cancellation included, and a rollback that itself fails
does not replace the error that caused it.

`_agent_search_path` called `get_agent_cache_dir()` eagerly, and that went
through `get_cache_dir()`, which creates the directory. An unwritable cache
location therefore raised out of `get_agent_binary` even when
DEFIB_AGENT_DIR held the binary, and out of the help text whose whole job is
to say that nothing was found. Building a search path is now free of side
effects; `get_cache_dir` gained `create=False` for callers that are only
looking.
@openipc-ai
openipc-ai merged commit 74b0ec0 into master Sep 8, 2026
13 checks passed
@openipc-ai
openipc-ai deleted the windows-tftp-and-agent-lookup branch September 8, 2026 05:35
openipc-ai added a commit that referenced this pull request Sep 8, 2026
test_real_netsh_yields_at_least_one_adapter failed all three windows-latest
jobs here, on code identical to what it passed on in #135. It asserted that
netsh lists at least one adapter, which is a property of the runner rather
than of anything we wrote -- so it passed once, failed the next time, and
told us nothing about the parser either way.

What it now asserts is ours: netsh is callable, its output parses, and
nothing that is not an adapter name comes back. A header row surviving the
shape test is the failure mode that would matter, and that shows up
regardless of what the host has plugged in. The deterministic tests against
a captured table still pin the parsing itself.

The failure did expose something real. The synchronous path ignored netsh's
exit status, unlike the async twin added in the same PR, so a netsh that
failed to run left us parsing empty stdout and reporting "no adapters" --
a command that did not run, presented as a host without a network. It
checks the exit status now, and a test pins that.
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