Skip to content

Add et --ctl: drive a session through a local control socket - #819

Merged
MisterTea merged 11 commits into
MisterTea:masterfrom
Kronuz:et-session-support
Sep 28, 2026
Merged

MisterTea merged 11 commits into
MisterTea:masterfrom
Kronuz:et-session-support

Conversation

@Kronuz

@Kronuz Kronuz commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

AI-assisted implementation.

Summary

et --ctl runs a session in the background with no local terminal, serving a per-user Unix socket that a program drives instead of a tty. It is an ordinary named session underneath, so --name reattaches it, it survives an etserver restart, and it appears in et --list beside any other.

ControlConsole is a Console, so TerminalClient::run() needs no changes: it selects on a pipe for injected input, and every TERMINAL_BUFFER it receives lands in a non-destructive scrollback that callers read by cursor. ControlListener serves the socket and answers requests for output, input, terminal size and status. The directory is 0700, the socket 0600, and the daemon checks the peer's uid.

The socket lives at ~/.et/control/<name>.sock, beside the saved-session directory rather than inside it. ~/.et/sessions/ is one file per session name, and a session may legally be called foo.sock, which is exactly where session foo wants its socket.

A session that ends records why next to its socket before the socket goes away, so the next request can tell "the server no longer has this session" from a name that never existed.

--ctl is POSIX-only and declines on Windows. Nothing here runs without it; et is unchanged in every other mode.

#820 adds etctl, the client for this socket.

Testing Done

  • 494 tests pass on macOS (ctest --parallel), and the changed C++ is clang-format clean.
  • ControlConsoleTest covers injected input reaching the pipe, output reaching scrollback through both write and a partial writeSome, resize reflected in getTerminalInfo, and a secret write redacted from the transcript.
  • ControlListenerTest drives the socket end to end for each request type.
  • Against a loopback etserver: a control session runs commands and reports exit codes; killing its client with SIGKILL and reopening the same name lands in the same shell with its working directory and exported environment intact.

@codecov

codecov Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.83333% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.90%. Comparing base (ca91fb5) to head (eac0d1d).

Files with missing lines Patch % Lines
src/terminal/ControlListener.cpp 71.54% 21 Missing and 14 partials ⚠️
src/terminal/ControlPaths.hpp 0.00% 16 Missing ⚠️
src/terminal/SessionTombstone.hpp 0.00% 16 Missing ⚠️
src/terminal/ControlProtocol.hpp 86.04% 6 Missing and 6 partials ⚠️
src/terminal/SessionTranscript.hpp 60.00% 6 Missing and 4 partials ⚠️
src/terminal/TerminalClient.hpp 0.00% 9 Missing ⚠️
src/terminal/ControlConsole.cpp 90.19% 1 Missing and 4 partials ⚠️
src/terminal/ControlConsole.hpp 50.00% 2 Missing ⚠️
src/terminal/SessionScrollback.hpp 95.45% 0 Missing and 2 partials ⚠️
test/integration_tests/ControlListenerTest.cpp 97.95% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #819      +/-   ##
==========================================
+ Coverage   79.86%   79.90%   +0.04%     
==========================================
  Files         151      162      +11     
  Lines       21872    22472     +600     
  Branches    14197    14556     +359     
==========================================
+ Hits        17467    17956     +489     
- Misses       2601     2679      +78     
- Partials     1804     1837      +33     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Kronuz
Kronuz marked this pull request as ready for review September 19, 2026 17:26
@Kronuz Kronuz changed the title Add named session reattachment and programmatic control to ET Add et --ctl: drive a session through a local control socket Sep 26, 2026
Keep bounded output and byte-exchange histories so control clients can
read session output and inspect traffic without owning the terminal.
Provide a console implementation that accepts programmatic input and
captures output, allowing ET sessions to run without a local terminal.
Expose session control through a per-user Unix socket and a shared
protocol. Test the listener against a real local socket connection.
Launch ET with a control console and listener in the background so
programs can drive the session through its control socket.
Take the control-session name from --name, and skip the connect-time
command when the connection recovered an already-running remote shell
rather than creating one. A control session is then just a named ET
session that happens to be driven through a socket, with one name, one
saved record, and one reconnect credential.
A control session's socket is unlinked when its daemon exits, so the
next etctl command sees ENOENT and cannot tell a session that ended
from one that never existed. Leave a note beside the session's saved
record naming the reason, and clear it when the name starts again.

TerminalClient::exitReason() turns the connection's state into that
sentence, and an unexpected accept() failure in the control listener
now logs its errno instead of breaking out as though shutdown had been
requested.
Return optional terminal dimensions and capture output through writeSome,
which the upstream terminal loop now uses instead of write. Without the
override, output is written to the read end of the control input pipe.

Update dimension checks and test that partial writes reach scrollback and
the transcript without being injected as terminal input.
ControlPaths.hpp included <dirent.h> unconditionally to list session
socket files; MSVC has no such header, so the Windows build failed
compiling TerminalClientMain.cpp. List the directory with
std::filesystem::directory_iterator instead, which is already used
elsewhere in the codebase and works on every supported platform.

ControlListener::listenFd was a plain int written by shutdown() on the
caller's thread while acceptLoop() read it on the accept thread with no
synchronization between the two. ThreadSanitizer flagged this as a data
race in the ControlListenerEndToEnd test (the kill scenario tears the
listener down right after a request completes). Make listenFd an
atomic<int> and read/write it through load()/exchange() so the two
threads no longer touch it concurrently without synchronization.
The hand-rolled parent-creating mkdir loop is POSIX-only (::mkdir takes
one argument on MSVC). Use std::filesystem::create_directories, which is
portable and already used elsewhere, and apply the 0700 bits only where
POSIX permissions mean something. Drop ensureDir0700, dead once the loop
is gone.
Upstream (MisterTea#817) removed CMake-level Windows test exclusions in favor
of every test compiling everywhere and SKIP()-ing platform-impossible
cases at runtime. Follow that convention instead of excluding these
files:

* ControlListenerTest exercises ControlListener end-to-end, which is
  compiled out entirely on Windows (--ctl is explicitly unsupported
  there); the whole test now SKIPs on Windows.
* ControlConsoleTest's pipe-based cases (getFd()/injectInput()) depend
  on ControlConsole's POSIX-only pipe and SKIP on Windows; its two
  pipe-free cases (scrollback, resize) already worked and are
  unchanged.
et reaches a live session two ways: bootstrapping a new one over SSH,
and reattaching to one already saved under --name. Only the first ran
the control listener, so a control session whose client had died was
reattached and then immediately killed: the reattach path builds a
terminal console and calls run() with noexit unset, which appends
'; exit'.

Hoist console creation and control-socket setup above the branch, and
give both paths one driver. A control session now behaves the same
however it got there.

Move the live control files (socket, session-end note) to ~/.et/control
rather than sharing ~/.et/sessions with the saved records. Sharing one
flat directory reads well until a name collides: a session may legally
be named 'foo.sock', which is exactly the path session 'foo' wants for
its socket, and a listing cannot tell the two apart without reserving
suffixes from the session namespace. A sibling directory leaves that
namespace entirely to SessionStore, which is unmodified.
@MisterTea
MisterTea merged commit 5b0f17a into MisterTea:master Sep 28, 2026
37 checks passed
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.

2 participants