Skip to content

feat(ssh): watch a remote host with nothing installed on it - #66

Merged
Mapika merged 1 commit into
mainfrom
feat/agentless-watch
Aug 1, 2026
Merged

Mapika merged 1 commit into
mainfrom
feat/agentless-watch

Conversation

@Mapika

@Mapika Mapika commented Aug 1, 2026

Copy link
Copy Markdown
Owner

The last item in Tier 2. portview ssh <host> watch --agentless now works, which means agentless mode covers every command.

One connection, not one per tick

The obvious implementation re-runs the probe each second. That pays an SSH handshake per refresh and re-authenticates every second, which is both slow and conspicuous in auth logs. Instead the probe loops on the far end and this side reads what it sends back.

No framing had to be invented for that — the probe already ends each record with #END, so the reader accumulates until it sees one. The exit 0 in the probe's no-collector branch ends the loop too, which is the right behaviour: nothing will change by trying again.

Verified rather than assumed, by counting invocations of a stubbed ssh:

==> refresh check (same row, 6s apart)
  t=3s: │ 3000 TCP 7 127.0.0.1 root node 3s 44 MB node /opt/app/web.js
  t=9s: │ 3000 TCP 7 127.0.0.1 root node 9s 44 MB node /opt/app/web.js
  RESULT: changed — the probe loop is feeding new records
  ssh invocations for the whole session: 1

Rendering the first record proves nothing about refresh, so the check is that uptime advances and the connection count stays at one.

Kill needed its own path

The remote kill ran run_oneshot(["kill", <port>]), which invokes portview on the remote — exactly what agentless mode does not have. It would have failed at the moment of use, on the one action with consequences. Agentless sessions now signal the PID the probe reported:

==> kill over SSH with no portview on the far end
  web.js pid=7, port 3000 present: 1
  after kill -TERM, port 3000 present: 0

The PID is a u32 parsed from the probe's own output, so it cannot carry shell syntax.

A failure the user could not see

Testing the no-collector path showed the error was invisible: setting a status message and quitting draws one frame, then the alternate screen is torn down and takes the message with it. The user got a blank exit with no reason.

Stream failures are now reported on stderr after the terminal is restored:

$ portview ssh devbox watch --agentless
remote host has neither portview, `ss`, nor `lsof`. Install portview there, or install iproute2 (Linux) or lsof.
$ echo $?
1

Same wording as the one-shot scan gives for the same host. This also fixes the pre-existing "Connection lost" case, which had the same problem.

Structure

RemoteFeed distinguishes the two framings — a remote portview's one-array-per-line versus the probe's #END-terminated records. Reading is the only part of the TUI that needs to know which, so read_remote_snapshot is the only thing that branches; everything downstream sees Vec<PortInfo>.

Also

README claimed watch requires portview remotely, and that macOS/BSD hosts still do too — the latter was already stale from #63. Both corrected.

181 tests, including feed-framing coverage (two records back to back, --all, the no-collector message, and the JSON feed unchanged). fmt/clippy clean, macOS + Windows type-check.

The probe loops on the far end and the TUI reads the records it emits, so a
session costs one SSH connection rather than one per tick. No framing needed
inventing: the probe already terminates each record with #END.

Killing signals the PID directly, since the installed-portview path shells
out to `portview kill` — precisely what is missing here.

A remote session that ends now reports why on stderr after the terminal is
restored. As a status line it rendered for one frame and then left with the
alternate screen.
@Mapika
Mapika merged commit 3de3e64 into main Aug 1, 2026
6 checks passed
@Mapika
Mapika deleted the feat/agentless-watch branch August 1, 2026 06:49
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