Skip to content

v0.2.0

Choose a tag to compare

@atomicstack atomicstack released this 23 Aug 04:31
· 7 commits to main since this release

control-mode framing correctness, safe shutdown, and per-command cancellation

this release fixes a protocol bug that could tear down the control connection from ordinary pane content, two concurrency defects on the shutdown path (one of them an unrecoverable panic), and adds context support so a wedged command can be abandoned. it also exposes the tmux 3.8 floating-pane formats.

fixes

  • the router treated %begin/%end/%error/%exit as protocol frames even inside an open block, so capture-pane -p output could be parsed as protocol. a captured line starting %exit tore down the entire connection; a captured %begin mis-bound the next queued request; a captured %end with a colliding command number completed a command early. the in-block check now runs first: while a block is open every line is command output until the exact closing guard arrives, matching the contract upstream tmux asserts in regress/control-notify-guard.sh after 6db5175e (a6f21f3)
  • guards are now matched as whole keywords, so %beginning, %errors and %exited are notifications again rather than malformed frames, and parseFrame requires exactly three decimal fields (a6f21f3)
  • %end/%error are matched on the full (time, number, flags) triple rather than the command number alone, and %exit is only honoured at depth 0 — tmux prints it from the client process after proc_loop returns, so inside a block it is always pane content (a6f21f3)
  • Tmux.Close() wrote router, transport and Socket with no synchronisation while runCommand read router, giving both a data race and a check-then-use nil dereference. the fields are no longer cleared at all: router.close() already fails every pending and in-flight request via failAll, so clearing them bought nothing but the race. Close is now idempotent and safe to call alongside in-flight commands (11bfb6d)
  • fixed a send on closed channel panic in the control transport: finish() closed the lines channel while the stdout forwarder — its only sender — could be parked on a send. the forwarder now owns the close. the panic landed on a library-owned goroutine, so consumers could not recover from it (995a77a)

additions

  • per-command cancellation via ListSessionsContext, ListAllWindowsContext, ListAllPanesContext, CapturePaneContext, ListSessionsFormatContext, ListWindowsFormatContext, ListPanesFormatContext and CommandContext. existing signatures are unchanged and delegate through context.Background(), so this is purely additive. the context reaches every command in the ListAllWindows/ListAllPanes fallback chains, not just the first (cbf3afb)
  • DefaultHandshakeTimeout (10s) and WithHandshakeTimeout bound the initial control-mode handshake, so a tmux that neither completes the handshake nor closes the transport can no longer wedge NewTmux/DefaultTmux forever. a non-positive duration disables the bound (c00feb1)
  • floating and modal pane formats, which tmux 3.8 otherwise leaves indistinguishable from tiled panes in list-panes output: Pane.FloatingFlag, ModalFlag, Z, Flags, X, Y, UnzoomedWidth, UnzoomedHeight, and Window.ModalPane, ManualWidth, ManualHeight. all additive, all zero-valued on tmux versions that do not know the formats (61b274b)

notes

  • the handshake bound is a behaviour change: a constructor that previously hung forever now returns an error after 10s. that is the point, but WithHandshakeTimeout(0) restores the old behaviour if you need it
  • abandoning a command via context is deliberately caller-side only. the command has already been written to tmux and cannot be unsent, so the request stays in the router's pending queue and its reply is discarded on arrival — removing it would desynchronise response correlation, which pairs each %begin with pending[0]
  • the go directive stays at 1.22; nothing here needs a newer toolchain

internal

  • seven list-and-convert methods and seven find-by-field lookups each hand-rolled the same loop per entity type; both families now share unexported generic helpers, removing 51 lines with no change to any exported declaration (1155447)
  • GetClientByTty was the only lookup returning its error unwrapped; it now wraps like the other six, so its error string has changed (the wrap uses %w, so errors.Is/As are unaffected) (1155447)

tests

  • the router framing reproduction, replayed from a real tmux next-3.8 stream, plus exact-triple matching, substring-prefix cases, malformed guard shapes and transport EOF without %exit (a6f21f3)
  • an end-to-end integration test that captures a pane displaying a control-mode transcript and asserts the connection survives it (a6f21f3)
  • Close driven concurrently with a command loop, failing under -race on the previous code (11bfb6d)
  • the transport forwarder parked on a full channel while the fake tmux writes to stderr and exits, reproducing the panic (995a77a)
  • cancellation and deadline coverage, including that an abandoned request does not desynchronise the router and that an already-cancelled context never reaches tmux (cbf3afb)
  • characterisation tests for six list/lookup functions that had 0% coverage before the refactor touched them (8d103a5)

upgrading

go get github.com/atomicstack/gotmuxcc@v0.2.0