Skip to content

ares-install: one transport per install, and three smaller fixes - #21

Merged
mariotaku merged 4 commits into
mainfrom
fix/install-followups
Aug 28, 2026
Merged

ares-install: one transport per install, and three smaller fixes#21
mariotaku merged 4 commits into
mainfrom
fix/install-followups

Conversation

@mariotaku

Copy link
Copy Markdown
Member

Four fixes to ares-install and the session setup it shares. The second one turned out to fix a deterministic install failure — it was proposed as tidying.

1. One transport per install, not four (3845984)

Every file operation went through the FileTransfer trait on the session, and each of those calls Transfer::open, which asks the device for the sftp subsystem and execs an sftp-server there. An install does four — mkdir, put, sha256sum, rm — so it paid four handshakes and four sftp-server spawns where one does. One of the four was pure waste: sha256sum runs over an exec channel and never touches the sftp session set up for it.

On a UP7560 the connection died during every single install. Five or six attempts, all reporting Failed to install: Fatal: Socket error: disconnected while the device installed the package anyway. Never intermittent — deterministic. With one transport instead of four:

old new
runs 5–6 3
failures 100% 0
result Socket error: disconnected exit 0 in 4–5s

Same device, same 4.8MB package, same session as the failures.

On that set both dropbear and its sftp-server live under /media/developer/apps — the tree an install writes into — so exec'ing four sftp-servers out of it across an install looks like what the device objected to. That mechanism is not proven. What is established is the correlation: four handshakes fail every time, one succeeds every time. The specific mechanisms that could be checked were eliminated — no channel cap in dropbear 2022.83, no service restart (listener pid and start time unchanged), no OOM kill, and no SIGBUS or other kernel fault over a dmesg window verified to cover the event.

Cleanup at the end of a run now actually happens rather than failing against a dead session. The test device had accumulated nine orphaned ares_install_*.ipk, 41.6MB, all predating this change.

The trade: one sftp session now has to survive the whole install rather than being reopened per step. On a device that drops the connection mid-install that costs nothing, since the session goes either way.

2. Keepalive on every session, not just a forward's (10e9984)

A device that goes away without closing the connection sends no FIN and no RST, so the host goes on believing in a socket that leads nowhere. ares-novacom learned to notice this; a forward is not the only thing that waits.

ares-install waits longer and more often. Its progress subscription loops while is_closed() and is_eof() are false — and on a half-open socket both stay false for good, so an install against a device that vanishes mid-progress hangs rather than failing.

Moved into connect, where every session in every tool passes through, and exposed as keep_alive for hand-built sessions. ares-novacom drops its own copy and socket2 moves with it, so there is one implementation rather than two.

It does not fire on a busy device: the probes are answered by the peer's TCP stack rather than by whatever it is running, so a device unpacking a package or swapping keeps answering. Checked — a peer frozen for 100s, well past the 30s + 3×10s window, survives and still works afterwards, while a peer that is gone is caught.

This is the widest-reaching change here: ares-shell, push, pull, launch and device all inherit it.

3. Compile the state patterns once (8b3c3a4)

map_installer_message runs for every progress line and built (?i)FAILED and (?i)^SUCCESS inside it, while both callers passed a freshly compiled pattern of their own — three regex compilations per line for an install, one per line for a remove, each with an unwrap on a hot path. Hoisted to LazyLock statics. No behaviour change.

4. Two more ways a run reported something it did not know (40cacad)

  • A remove whose stream ended having said neither "removed" nor a failure reached result.unwrap() on a None and panicked. Same silence install already handles.
  • An install whose success message carries no packageId printed Installed package ! with an empty name.

Verification

Builds clean; 79 tests pass; clippy 56 → 55 workspace-wide.

Fix 1 is confirmed on hardware by the session working on the app — that is the 3/3 above. Not exercised by anyone: --remove, the empty-packageId path, and the keepalive timeout.

One caveat on attribution: the hardware test ran a build containing all four commits, not fix 1 alone. The others cannot prevent a disconnect — keepalive only detects a dead socket, and 3 and 4 are inert — so fix 1 is the only candidate, but it was not isolated under test.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Gde2tcn5SbUHKhesuTNB7a

Mariotaku and others added 4 commits August 28, 2026 15:15
A device that goes away without closing the connection sends no FIN and no
RST, so the host goes on believing in a socket that leads nowhere. Whoever
is waiting to read then waits forever. ares-novacom learned to notice this
because a forward sat two hours against a TV that had stopped listening,
but a forward is not the only thing that waits.

ares-install waits longer and more often. Its progress subscription reads
in a loop while `is_closed()` and `is_eof()` are false - and on a
half-open socket both stay false for good, so an install against a device
that vanishes mid-progress hangs rather than failing.

Move the keepalive into `connect`, where every session in every tool
passes through, and expose it for anyone building a session by hand. The
probes are answered by the peer's TCP stack rather than by whatever it is
running, so a device that is merely busy - unpacking a package, or
swapping - keeps answering and is left alone. That is checked: a peer
frozen for 100s, well past the 30s + 3x10s detection window, survives and
still works afterwards, while a peer that is gone is caught.

ares-novacom drops its own copy, and socket2 moves with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gde2tcn5SbUHKhesuTNB7a
Every file operation went through the FileTransfer trait on the session,
and each of those calls Transfer::open, which asks the device for the sftp
subsystem and execs an sftp-server there. An install does four of them -
mkdir, put, sha256sum, rm - so it paid four handshakes and four
sftp-server spawns where one would do. One of the four was pure waste:
sha256sum runs over an exec channel and never touches the sftp session
that had just been set up for it.

This started as tidying. It turns out to fix a hard failure.

On a UP7560 the connection died during every single install: five or six
attempts, all reporting "Failed to install: Fatal: Socket error:
disconnected" while the device installed the package anyway. It was never
intermittent - it was deterministic. With one transport instead of four it
is deterministically fine: three consecutive clean installs, exit 0 in 4-5
seconds, same device, same package, same session as the failures.

On that set both dropbear and its sftp-server live under
/media/developer/apps - the tree an install writes into - so exec'ing four
sftp-servers out of it across an install looks like what the device
objected to. That mechanism is not proven. The correlation is: four
handshakes fail every time, one succeeds every time.

The cleanup at the end of a run now actually happens rather than failing
against a dead session. That device had accumulated nine orphaned
ares_install_*.ipk, 41.6MB, every one of them from before this change.

Hardware result contributed by the session working on the app; the trade
is that one sftp session now has to survive the whole install rather than
being reopened per step, which on a device that drops the connection
mid-install costs nothing, since the session goes either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gde2tcn5SbUHKhesuTNB7a
`map_installer_message` runs for every progress line the device sends, and
built `(?i)FAILED` and `(?i)^SUCCESS` inside it, while both callers passed
a freshly compiled pattern of their own. That is three regex compilations
per line for an install and one per line for a remove, each with an
`unwrap` that could panic on a hot path.

Hoist all four into `LazyLock` statics and share them. No behaviour
change; the patterns are identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gde2tcn5SbUHKhesuTNB7a
Both are the silent-stream case that install already handles, in the two
places it was still unhandled.

A remove whose stream ended having said neither "removed" nor a failure
reached `result.unwrap()` on a None and PANICKED. Same silence, same
answer as install's: report that the device stopped talking.

An install whose success message carries no packageId printed "Installed
package !" with an empty name. The device does not always name what it
installed; say so plainly rather than leaving a hole in the sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gde2tcn5SbUHKhesuTNB7a
@mariotaku
mariotaku merged commit bb5ce08 into main Aug 28, 2026
4 checks passed
@mariotaku
mariotaku deleted the fix/install-followups branch August 28, 2026 11:15
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