Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,15 @@ add_library(
src/terminal/MuxClient.cpp
src/terminal/TitleParser.hpp
src/terminal/TitleParser.cpp
src/terminal/ControlConsole.hpp
src/terminal/ControlConsole.cpp
src/terminal/ControlListener.hpp
src/terminal/ControlListener.cpp
src/terminal/ControlProtocol.hpp
src/terminal/ControlPaths.hpp
src/terminal/SessionScrollback.hpp
src/terminal/SessionTombstone.hpp
src/terminal/SessionTranscript.hpp
src/terminal/ServerFifoPath.hpp
src/terminal/SshSetupHandler.hpp
src/terminal/SshSetupHandler.cpp
Expand Down
104 changes: 104 additions & 0 deletions src/terminal/ControlConsole.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include "ControlConsole.hpp"

#include "RawSocketUtils.hpp"

namespace et {

namespace {
const int kDefaultRows = 24;
// 132 columns is the standard "wide" terminal (the VT100/VT220 132-column
// mode); it is a better default than 80 for a session whose viewer hasn't sized
// it yet, since attach/observe only learn the real width once someone connects.
const int kDefaultCols = 132;

int64_t nowSeconds() { return (int64_t)time(NULL); }
} // namespace

ControlConsole::ControlConsole(size_t scrollbackCapBytes)
: scrollback(scrollbackCapBytes),
lastActivityTime(nowSeconds()),
createdTime(nowSeconds()) {
inputPipe[0] = inputPipe[1] = -1;
#ifndef WIN32
FATAL_FAIL(::pipe(inputPipe));
/*
* Non-blocking read end: run() probes it via select(), and a spurious wakeup
* must never block the loop.
*/
int flags = fcntl(inputPipe[0], F_GETFL, 0);
fcntl(inputPipe[0], F_SETFL, flags | O_NONBLOCK);
#endif
size.set_row(kDefaultRows);
size.set_column(kDefaultCols);
size.set_width(0);
size.set_height(0);
}

ControlConsole::~ControlConsole() {
#ifndef WIN32
if (inputPipe[0] >= 0) {
::close(inputPipe[0]);
}
if (inputPipe[1] >= 0) {
::close(inputPipe[1]);
}
#endif
}

std::optional<TerminalInfo> ControlConsole::getTerminalInfo() {
lock_guard<std::mutex> guard(sizeMutex);
return size;
}

int ControlConsole::getFd() { return inputPipe[0]; }

void ControlConsole::write(const string& s) {
// run() calls this with each TERMINAL_BUFFER of server output.
scrollback.append(s);
transcript.append('<', s);
touchActivity();
}

size_t ControlConsole::writeSome(const string& s) {
write(s);
return s.size();
}

void ControlConsole::injectInput(const string& bytes, bool secret) {
#ifndef WIN32
if (inputPipe[1] < 0 || bytes.empty()) {
return;
}
RawSocketUtils::writeAll(inputPipe[1], &bytes[0], bytes.length());
transcript.append('>', secret ? string("<secret>") : bytes);
touchActivity();
#endif
}

ScrollbackRead ControlConsole::readOutput(int64_t cursor) const {
return scrollback.read(cursor);
}

TranscriptRead ControlConsole::readTranscript(int64_t cursor) const {
return transcript.read(cursor);
}

void ControlConsole::setSize(int row, int column, int width, int height) {
lock_guard<std::mutex> guard(sizeMutex);
size.set_row(row);
size.set_column(column);
size.set_width(width);
size.set_height(height);
}

int64_t ControlConsole::lastActivity() const {
lock_guard<std::mutex> guard(activityMutex);
return lastActivityTime;
}

void ControlConsole::touchActivity() {
lock_guard<std::mutex> guard(activityMutex);
lastActivityTime = nowSeconds();
}

} // namespace et
90 changes: 90 additions & 0 deletions src/terminal/ControlConsole.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#ifndef __ET_CONTROL_CONSOLE_HPP__
#define __ET_CONTROL_CONSOLE_HPP__

#include "Console.hpp"
#include "Headers.hpp"
#include "SessionScrollback.hpp"
#include "SessionTranscript.hpp"

namespace et {

/*
* A Console implementation that drives an ET session programmatically instead
* of from a local TTY. It is the seam that lets the *unmodified*
* TerminalClient::run() loop be controlled by an external tool (etctl):
*
* * getFd() returns the read end of an internal pipe. run() selects on it
* and forwards whatever appears as TERMINAL_BUFFER input, so injectInput()
* bytes (keystrokes, 0x03, 0x04, escape sequences, anything) reach the remote
* PTY exactly as if typed.
* * write() is the output sink: run() hands every TERMINAL_BUFFER it receives
* here, and we append the raw bytes to a non-destructive scrollback that
* readOutput() serves by cursor.
* * getTerminalInfo() returns a settable size; setSize() changes it, and
* run() emits a TERMINAL_INFO (resize) on its next poll.
* * setup()/teardown() are no-ops: there is no TTY to put in raw mode.
*
* The pipe is kernel-synchronized; the scrollback is internally locked; the
* size is guarded here. run() touches this object on its main thread (getFd
* reads, write, getTerminalInfo); the control listener touches it on another
* thread (injectInput, readOutput, setSize) — a clean producer/consumer split.
*/
class ControlConsole : public Console {
public:
explicit ControlConsole(
size_t scrollbackCapBytes = SessionScrollback::kDefaultCapBytes);
virtual ~ControlConsole();

// --- Console interface (called by TerminalClient::run on its main thread)
// ---
std::optional<TerminalInfo> getTerminalInfo() override;
virtual void setup() {}
virtual void teardown() {}
virtual int getFd();
virtual void write(const string& s);
size_t writeSome(const string& s) override;

// --- Control surface (called by the control listener thread) ---

// Inject raw bytes as terminal input (returned to run() via getFd()). When
// `secret` is set the bytes still reach the shell, but the transcript records
// a redacted placeholder so a `peep` never reveals a typed password.
void injectInput(const string& bytes, bool secret = false);

// Read session output at `cursor` without consuming it.
ScrollbackRead readOutput(int64_t cursor) const;

// Read the direction-tagged transcript (input + output) at a record cursor.
TranscriptRead readTranscript(int64_t cursor) const;

// Set the logical terminal size; run() will propagate it as a resize.
void setSize(int row, int column, int width = 0, int height = 0);

// The live output cursor (offset just past the most recent byte).
int64_t headCursor() const { return scrollback.headCursor(); }

// Seconds since the epoch of the last input or output activity.
int64_t lastActivity() const;

// Seconds since the epoch when this session was created.
int64_t createdAt() const { return createdTime; }

protected:
SessionScrollback scrollback;
SessionTranscript transcript;

int inputPipe[2]; // [0] read end (getFd), [1] write end (injectInput)

mutable std::mutex sizeMutex;
TerminalInfo size;

mutable std::mutex activityMutex;
int64_t lastActivityTime;
int64_t createdTime;

void touchActivity();
};

} // namespace et

#endif // __ET_CONTROL_CONSOLE_HPP__
Loading
Loading