Add et --ctl: drive a session through a local control socket - #819
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
Kronuz
marked this pull request as ready for review
September 19, 2026 17:26
Kronuz
force-pushed
the
et-session-support
branch
from
September 19, 2026 17:33
ee73f56 to
268b6a1
Compare
This was referenced Sep 19, 2026
Kronuz
force-pushed
the
et-session-support
branch
from
September 26, 2026 19:54
268b6a1 to
5e389ce
Compare
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.
Kronuz
force-pushed
the
et-session-support
branch
from
September 27, 2026 14:02
5e389ce to
eac0d1d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
et --ctlruns 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--namereattaches it, it survives anetserverrestart, and it appears inet --listbeside any other.ControlConsoleis aConsole, soTerminalClient::run()needs no changes: it selects on a pipe for injected input, and everyTERMINAL_BUFFERit receives lands in a non-destructive scrollback that callers read by cursor.ControlListenerserves the socket and answers requests for output, input, terminal size and status. The directory is0700, the socket0600, 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 calledfoo.sock, which is exactly where sessionfoowants 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.
--ctlis POSIX-only and declines on Windows. Nothing here runs without it;etis unchanged in every other mode.#820 adds
etctl, the client for this socket.Testing Done
ctest --parallel), and the changed C++ isclang-formatclean.ControlConsoleTestcovers injected input reaching the pipe, output reaching scrollback through bothwriteand a partialwriteSome, resize reflected ingetTerminalInfo, and a secret write redacted from the transcript.ControlListenerTestdrives the socket end to end for each request type.etserver: a control session runs commands and reports exit codes; killing its client withSIGKILLand reopening the same name lands in the same shell with its working directory and exported environment intact.