Skip to content

feat(serial): close/reopen the port and gate auto-reconnect (#192) - #193

Open
kipavy wants to merge 1 commit into
devfrom
feat/serial-port-control
Open

feat(serial): close/reopen the port and gate auto-reconnect (#192)#193
kipavy wants to merge 1 commit into
devfrom
feat/serial-port-control

Conversation

@kipavy

@kipavy kipavy commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #192.

The problem

A serial console holds the port for as long as its tab is open, and a drop starts the reconnect backoff, which reclaims the port every few seconds for ~3 minutes. Both fight the flashing tool on a board like an ESP32: today the only way to free /dev/ttyUSB0 is to close the whole tab, and even then a stale loop can take the port back before esptool gets it.

What changed

Two buttons in the serial status bar:

  • Plug toggle — releases the port but keeps the tab, its scrollback and its config; the next click reopens it. The close marks the session disconnected before releasing the port, so the serial-closed event that follows is not read as a drop worth chasing (handleSessionClosed only starts the loop for a session that still reads connected).
  • Auto-reconnect toggle — stored as serial_auto_reconnect on the connection, so it survives a restart and follows the device rather than the tab. It is also editable in the serial connection form. Off means a drop leaves the port free and the reopen button armed. Quick-connect serial sessions carry a sentinel connection id nothing resolves, so there the preference lives on the session for its lifetime.

Default is on, and undefined reads as on, so existing connections behave exactly as before.

Backend defect found on the way

Reopening a port left the previous read thread alive on its cloned descriptor: two threads would split the device's output between them, and the older one's serial-closed would tear down the session that replaced it. This predates the change, but a reopen button makes it reachable in one click. Sessions now carry a generation that a read thread checks before continuing and before reporting a close.

Refactors carried along

  • StatusBarIconButton in TerminalStatusBar — the SSH and serial reconnect buttons were the same button written twice; the serial copy is gone and the SSH one now uses the shared component.
  • SettingRow in formShared — the icon + label + right-hand control row existed 7 times in ConnectionForm; all of them now use the shared component instead of this change adding an 8th.
  • markSessionDisconnected next to the existing markSessionConnecting / markSessionError helpers.

Verification

Gates, all from the worktree:

  • tsc --noEmit — exit 0
  • vitest run — 516 files, 3964 tests, 0 failures
  • cargo test — 342 passed, 0 failed
  • cargo fmt --all --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — exit 0

New tests: the auto-reconnect gate in reconnectBackoff.test.ts, the serialAutoReconnectEnabled resolver, closeSerialPort / setSerialAutoReconnect in sessionStore.serialPort.test.ts (including that the session is marked disconnected before the port is released), and the generation guard in serial/connect_tests.rs via TTYPort::pair().

Live-verified in a headless container against an emulated device — a pty published as /dev/ttyUSB99, printing a line a second, with SIGUSR1 as "unplug":

  1. Close — status goes disconnected, tab and config survive, and 0 output frames arrive over the next 3s while the button flips to "Reopen". The backoff never reclaimed the port.
  2. Reopen — status connected, 6 output frames in 4s, button flips back to "Close".
  3. Auto-reconnect off, device unplugged — status goes disconnected within 200ms and stays disconnected for the full 14s watch. The toggle is written to the connection (serial_auto_reconnect: false).
  4. Control: auto-reconnect on, same unplug — status goes connecting and the loop recovers to connected on its own once the device returns.

Not proven here: the actual "port busy" contention. A pty does not enforce exclusive access the way a USB serial driver does, so the emulator can show that Voltius stops holding and stops reclaiming the port, but not that esptool then wins it. That last step needs real hardware.

A serial console holds /dev/ttyUSB0 for as long as its tab is open, and a
drop starts the reconnect backoff, which reclaims the port every few
seconds. Both fight the flashing tool on a board like an ESP32: the user
has to close the whole tab to flash, and even then the loop can take the
port back before esptool gets it.

The serial status bar now carries two buttons:

- A plug toggle that releases the port while keeping the tab, its
  scrollback and its config, and reopens it on the next click. The close
  marks the session disconnected before releasing, so the resulting
  serial-closed event is not read as a drop worth chasing.
- An auto-reconnect toggle, stored on the connection so it survives a
  restart and follows the device. Off means a drop leaves the port free
  and the reopen button armed. Quick-connect sessions have no connection
  to persist it on and keep it for the session's lifetime instead.

Reopening also exposed a defect in the backend: the previous read thread
outlives its port on a cloned descriptor, so two threads would split the
device's output and the older one's close would tear down the session
that replaced it. Sessions now carry a generation a thread checks before
continuing or reporting a close.
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