Coder Social home page Coder Social logo

whisperity / monomux Goto Github PK

View Code? Open in Web Editor NEW
17.0 4.0 2.0 710 KB

Monophone Terminal Multiplexer - Less intrusive than tmux, smarter than screen

License: GNU General Public License v3.0

CMake 4.69% C++ 94.54% C 0.77%
linux console terminal ssh detach terminal-application screen tmux posix unix

monomux's Introduction

MonoMux

MonoMux (for Monophone Terminal Multiplexer โ€” pun intended) is a system tool that allows executing terminal sessions in the background with on-demand attaching to them.

๐Ÿ“ข Important! MonoMux is in active incremental development! You are welcome using it as your daily driver, but for long-term production systems, use the proven alternatives instead!

MonoMux is a tool similar to screen and tmux. It allows most of the core features of screen or tmux, with being less intrusive about its behaviour when it comes to using these tools with modern terminal emulators.

โš ๏ธ Warning! Currently, MonoMux is designed with only supporting Linux operating systems in mind. Most of the project is written with POSIX system calls in mind, but there are some GNU extensions used.

โ„น๏ธ Note: MonoMux is NOT a terminal emulator by itself! To use it, you may use any of your favourite terminal emulators.

Dependencies

MonoMux uses modern C++ features, and as such, a C++17-capable compiler and associated standard library is needed to compile and execute the tool. There are no other dependencies.

Installation

Ubuntu (18.04, 20.04, 22.04)

Download the .deb (and optionally the .ddeb) file for the release, and install the standard way:

sudo dpkg --install monomux-*.*deb

Alternatively, a .tar.gz archive might be used. The application consists of a single self-contained binary.

Usage

The easiest use of MonoMux is simply starting it: monomux. By default, a server starts in the background, and a default session is created with the default shell of the current user, and the client automatically attaches to this session. Executing the client with a server already running will attach to the only session on the server, or if multiple sessions exist, an interactive menu will start with which a session can be selected. (The interactive menu can be explicitly requested, even if only at most one session exists, with the -i or --interactive parameter.)

โ„น๏ธ Note: Please always refer to the output of monomux -h for up-to-date information about what flags the installed tool supports.

To run multiple independent servers, specify the -s/--socket option with a path on the file system. Communication between the server and the client takes place on this socket.

Fine-tuning Client options

The client can be fine-tuned during its start-up with several flags:

  • monomux /bin/myshell will start the specified program without passing any arguments to it.
  • monomux -- /bin/myshell -a -b --arg-to-shell will start the specified program with command-line arguments, if a new session is created. (If the started program takes - or -- arguments, an explicit separator -- must be given BEFORE the program's name!)
  • monomux -n SESSION_NAME will attach or start the session SESSION_NAME, bypassing the interactive menu.
  • Environment variables can be specified or removed via -e VARIABLE=Value -u UNSET_VARIABLE.

A server can be started explicitly via monomux --server, in which case no client creation and attachment will be done.

Why?

One of the most important contexts where screen or tmux comes to mind is over remote sessions. If a remote connection breaks โ€” or the local graphical terminal closes or crashes โ€”, the terminal session behind the connection is sent a SIGHUP signal, for which most programs exit. This results in the loss of shell history, and the interrupt of running programs.

The most crucial problem from an interactive work's point-of-view with existing tools is that both screen and tmux act as terminal emulators themselves. Their behaviour is to parse the VT sequences of the output received from the "remote" terminal and emit them to the attached client(s). Programs using extensive modern, or terminal specific features will have to fall back to the older and more restrictive set of what screen or tmux understands.

A fork of screen, dtach was created which emulates only the attach/detach features of screen. However, dtach has a straightforward and non-trivial interface, e.g. the user must specify the connection socket file manually.

(Moreover, all of the aforementioned tools are written in C.)

MonoMux aims to combine the good aspects of all of these tools but remove almost all of the possible hurdles in the way of tools running in the background session.

  • Attach/detach features are supported without the need of parsing control sequences.
  • Every I/O operation to and from the client to the attached session is passed verbatim, without understanding the contents.
    • This allows using all the features of a modern terminal emulator as-is.
    • However, this also means that features found in tmux such as splits or keybinds can not and will not be implemented in this tool.
  • Better defaults than dtach: no need to specify an exit escape sequence or modern resize events.
  • Like tmux, there is meaningful session management by default, giving an interactive attach menu if multiple sessions exist.

Written in C++17, with object-oriented design in mind. This might result in a larger binary than for other tools, however, MonoMux is intended for user systems, not embedded contexts.

monomux's People

Contributors

whisperity avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

monomux's Issues

Detect whether the session is "safe" to attach/detach

With in-line detachment getting implemented as part of #3, it should be noted that there are cases when detaching from a running session is not safe to the client's terminal (due to us having no terminal emulation and no replay of display buffers...). Most notable of these is the Mouse events:

  • Open vim (with a mouse-capable TERM set) behind monomux
  • Force-exit the session without backgrounding vim
  • Get clicky in the now detached terminal and observe:
2;64;18M2;64;18m2;99;27M0;99;27M2;99

Due to this, it is generally not recommended to detach without backgrounding a "full-fledged" terminal application (like vim or mc) first. Pre-#3, having to explicitly execute another binary sort of forced this behaviour from users. But nothing stops one client from attaching to an existing session where a feature application was running, in which case the new session did NOT receive the terminal state properly (and thus, e.g., mouse events were only happening from one session).

The server should be made capable of intercepting the output stream and understanding some control sequences mostly related to changing modes in the underlying terminal. With this, we could approximate what state the session is in, and be able to mark it as "unsafe". If the session is currently "unsafe", we should not let the user detach, and also show a warning when trying to attach.

Generate the serialisation logic in the messaging layer with tooling instead of the custom case-by-case logic

Currently, all of the serialisation logic for the control message primitives are all built on top of hand-crafted code:

ENCODE_BASE(ProcessSpawnOptions)
{
std::ostringstream Buf;
Buf << "<PROCESS>";
{
Buf << "<IMAGE>" << Object.Program << "</IMAGE>";
Buf << "<ARGUMENTS Count=\"" << Object.Arguments.size() << "\">";
{
for (const std::string& Arg : Object.Arguments)
Buf << "<ARGUMENT Size=\"" << Arg.size() << "\">" << Arg
<< "</ARGUMENT>";
}
Buf << "</ARGUMENTS>";
Buf << "<ENVIRONMENT>";
{
Buf << "<DEFINE Count=\"" << Object.SetEnvironment.size() << "\">";
{
for (const std::pair<std::string, std::string>& EnvKV :
Object.SetEnvironment)
{
Buf << "<VARVAL>";
{
Buf << "<VAR Size=\"" << EnvKV.first.size() << "\">" << EnvKV.first
<< "</VAR>";
Buf << "<VAL Size=\"" << EnvKV.second.size() << "\">"
<< EnvKV.second << "</VAL>";
}
Buf << "</VARVAL>";
}
}
Buf << "</DEFINE>";
Buf << "<UNSET Count=\"" << Object.UnsetEnvironment.size() << "\">";
{
for (const std::string& EnvK : Object.UnsetEnvironment)
Buf << "<VAR Size=\"" << EnvK.size() << "\">" << EnvK << "</VAR>";
}
Buf << "</UNSET>";
}
Buf << "</ENVIRONMENT>";
}
Buf << "</PROCESS>";
return Buf.str();
}
DECODE_BASE(ProcessSpawnOptions)
{
ProcessSpawnOptions Ret;
HEADER_OR_NONE("<PROCESS>");
CONSUME_OR_NONE("<IMAGE>");
EXTRACT_OR_NONE(Image, "</IMAGE>");
Ret.Program = Image;
{
CONSUME_OR_NONE("<ARGUMENTS Count=\"");
EXTRACT_OR_NONE(ArgumentCount, "\">");
std::size_t ArgC = std::stoull(std::string{ArgumentCount});
Ret.Arguments.resize(ArgC);
for (std::size_t I = 0; I < ArgC; ++I)
{
CONSUME_OR_NONE("<ARGUMENT Size=\"");
EXTRACT_OR_NONE(ArgumentSize, "\">");
if (std::size_t S = std::stoull(std::string{ArgumentSize}))
Ret.Arguments.at(I) = splice(View, S);
CONSUME_OR_NONE("</ARGUMENT>");
}
CONSUME_OR_NONE("</ARGUMENTS>");
}
{
CONSUME_OR_NONE("<ENVIRONMENT>");
{
CONSUME_OR_NONE("<DEFINE Count=\"");
EXTRACT_OR_NONE(EnvDefineCount, "\">");
std::size_t SetC = std::stoull(std::string{EnvDefineCount});
Ret.SetEnvironment.resize(SetC);
for (std::size_t I = 0; I < SetC; ++I)
{
CONSUME_OR_NONE("<VARVAL>");
CONSUME_OR_NONE("<VAR Size=\"");
EXTRACT_OR_NONE(VarSize, "\">");
if (std::size_t S = std::stoull(std::string{VarSize}))
Ret.SetEnvironment.at(I).first = splice(View, S);
CONSUME_OR_NONE("</VAR>");
CONSUME_OR_NONE("<VAL Size=\"");
EXTRACT_OR_NONE(ValSize, "\">");
if (std::size_t S = std::stoull(std::string{ValSize}))
Ret.SetEnvironment.at(I).second = splice(View, S);
CONSUME_OR_NONE("</VAL>");
CONSUME_OR_NONE("</VARVAL>");
}
CONSUME_OR_NONE("</DEFINE>");
}
{
CONSUME_OR_NONE("<UNSET Count=\"");
EXTRACT_OR_NONE(EnvUnsetCount, "\">");
std::size_t UnsetC = std::stoull(std::string{EnvUnsetCount});
Ret.UnsetEnvironment.resize(UnsetC);
for (std::size_t I = 0; I < UnsetC; ++I)
{
CONSUME_OR_NONE("<VAR Size=\"");
EXTRACT_OR_NONE(VarSize, "\">");
if (std::size_t S = std::stoull(std::string{VarSize}))
Ret.UnsetEnvironment.at(I) = splice(View, S);
CONSUME_OR_NONE("</VAR>");
}
CONSUME_OR_NONE("</UNSET>");
}
CONSUME_OR_NONE("</ENVIRONMENT>");
}
BASE_FOOTER_OR_NONE("</PROCESS>");
return Ret;
}

While arguably this is the only part of the project that's actually tested, the logic behind the communication system follows a clear pattern, and there are not many data types that are generally transmitted (things are either strings, numbers, tuples, or an array of these...).

This means that the communication layer, the full serialisation logic, and perhaps even the automated tests for it (!) could be generated automatically from a sufficiently concise DSL. We should steer clear of heavyweight communication protocol libraries (such as Apache Thrift) or DSL parsers that depend on compiler-grade tooling (such as ODB implemented as a custom GCC frontend action) as such tools might make our compilation process too complex.

While this could be implemented as a fully backwards-compatible change, it might be worthwhile to do this together with (or as a precondition of) #17 and several other planned communication layer changes (e.g., #14). We might realise during making this that a different format is more suitable for comfortable automatically generated serialisation, and it would be great only to break ABI once if we can afford it.

Action: Detach from the running session

Instead of having to call the monomux binary with a specific flag, the client itself should be able to send a request to detach, which would result in a graceful detachment (instead of a "kicked" message) of the client that sent the request.

Implementing this could also obsolete the -d/--detach flag. (Likely -D/--detach-all should be kept for scripting purposes.)

Recreate the socket file in case it is deleted

Idea was taken from abduco

This could perhaps even be extended with the client at connection time finding all monomux processes and instructing them to recreate their socket BEFORE autoforking a server to the background, so the user does not have to manually send a signal at all.

Allow setting up auto-injected variables (`TERM`, `SSH_AUTH_SOCK`, `DISPLAY`, ...)

We should support adding environment variables at session start-up and attach time to a list of obligatory injected variables that are stored for the session. (A good starting candidate for default of this list is TERM, SSH_AUTH_SOCK and COLORTERM, but maybe even DISPLAY.)

When attaching to a session, the newest attaching client must be queried about the values of these variables and the running session's environment should be set with these values. This part might require #11.

Interactive prompt stuck on infinite prinout loop after `C-d`

Steps to reproduce

  1. Run monomux -i
  2. Press Ctrl+d

Expected Behavior:

Monomux handles ctrl+d elegantly (personally I would like monomux to just stop expecting any input and quit without any error code).

Actual Behavior:

Monomux gets stuck in an infinite loop and keeps printing

Monomux sessions on '/run/user/1000/mnmx'...

    1. Create a new session (/bin/fish)
    2. Quit

Choose 1-2:
ERROR: Invalid input

Shell integration

Create a way to automatically execute bits 'n bobs of code at both session creation time, and at "sequence points" (such as prompt generations) within the session.

Support at least both Bash and Zsh.

Note: Envprobe already has this figured out. ๐Ÿ˜‰

Can't generate kitty image inside a session.

I was poking around to see if I could script a session management around monomux and ran into this:

โฏ kitty +kitten icat 2022-08-18-130820_1025x812_scrot.png
Error: Terminal does not support reporting screen sizes in pixels, use a terminal such as kitty, WezTerm, Konsole, etc. that does.

Running this in the same terminal window but with monomux killed works.

Support keybind-based (instead of `ControlClient`-based) actions

There are several valid use cases in which the user's shell (inside a session) is not able to communicate directly with the server that is executing said session. The two most important of such are when the user sus away (and no longer has write access to the socket) and when the session is running an SSH (or any other remote-ish) connection where the prompt the user can type in no longer has access to the socket. (In the latter case, even access to the monomux binary might be unreliable.)

We should support some rudimentary keybind-based (e.g., screen's or tmux's C-a) system where the user can bind a shortcut to a commonly used action.

Action: Switch sessions "in-line"

This would actually be a detach (#3) followed by re-running the already existing session-attachment logic, but without restarting the server.

Create and maintain custom symbolic link to a recent, valid `SSH_AUTH_SOCK`

Trivially implements #12 in practice.

Note: Idea taken from byobu.

A common problem occurs over elongated uses of Monomux where the user forwards the SSH Agent and starts a session with the environment in one forwarded connection (SSH_AUTH_SOCK=/tmp/sshd-..../agent). When the connection terminates, the Agent socket is deleted, but the running session still points to it. The next connection just attaches to the session and tries to use the invalidated agent.

At least for the common case of SSH (luckily the Agent is a socket file that we can symlink and such...) we should create a path which we inject as SSH_AUTH_SOCK into the spawned sessions. Every time a client attaches to the server successfully, we should ask the client what its SSH_AUTH_SOCK is, and during the connection process, set the symlink to this path.

This way, the sessions will always see the latest attached client's SSH Agent, which might not be the best solution if multiple connections exist in parallel, but still somewhat better than only the first connection working...

Livelock if attaching a session in a circular way

  1. Create and attach to session A
  2. From within A, create and attach to session B
  3. From within B, attach to session A

Both clients' and the server's CPU use will spike once it starts communicating data between the sessions recursively, resulting in a livelock of the affected sessions. On powerful enough systems, it seems that scheduling and queueing allow for other sessions to survive this ordeal.

Note: It is enough if session A recursively attaches to session A.

It is not trivial to detect and keep track of what session is attached to what other session, and while it could technically be possible, it is not worth the effort. A blanket denial of recursive Monomux attaches should be implemented instead. (This is how tmux does it, by the way: "[...] sessions should be nested with care, unset TMUX and try again [...]".)

Use host writable statusline (VT feature) to show monomux state to the user

See contour-terminal/contour#687 for some general insights.

I'd like monomux to make use of the host writable status line. This is currently supported by recent(!) xterm as well as Contour.

You can detect support by the terminal using DECQRM.

Information I'd like to see (up to your imagination):

  • What session am I connected to?
  • if multiple sessions are available, maybe list the others (by index or short title or so)

Given the last bullet point, it might make sense to be able to rename sessions (just like in tmux :D)

EDIT: forgot to mention: if anything is missing in Contour, please ping back to me.

With an integration feature, auto-switch the `TERM` of the attached session to the `TERM` of the client

Depends on #11
Depends on #12

I need a shell integration that lets attaching monomux clients tell the session (and the shell within it) to change TERM to the setting of the attaching client...

Seems like a moot and meek thing, but this could save an additional export TERM= every time machines are changed. Although changing the TERM of any already running but more complex process (e.g., neovim?) might bring issues and should just warrant a restart.

Also, this could be another "unsafe" flag (see #5) when two different clients (of different TERM behind them...) want to attach to the same session.

Version the communication protocol and prevent connection when client and server had breaking comms changes

A lot of currently pending issues (#2, #5, #12, #14) will require changes to the communication protocol which would be backwards-incompatible. This would be the perfect moment to introduce, into the comms, a stable way of the server and the client discussing the versions of the protocol used. There should be a soft warning in case the server and the client are not the same binaries (string comparing Version::whatever), and a hard error preventing communication if the API had breaking changes.

The API version should be versioned in a single global location in the code. A single unsigned number is enough... and we will hopefully keep updating it each time the communication layer changes.

An alternative solution would be creating a hash, at compile-time, from the most important and critical parts of the communication library. However, that would be too brittle and sensitive to refactoring changes in the source code itself that are irrelevant to the actual API at hand.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.