Skip to content

Latest commit

 

History

History
132 lines (95 loc) · 5.21 KB

File metadata and controls

132 lines (95 loc) · 5.21 KB

Choosing how commands reach tmux

Every Java snippet here is executed by a test: ExamplesTest for the ones that build something, ExecutionModeConformanceTest for the ones about precedence.

One switch, chosen once:

ServerConfig config = ServerConfig.builder()
        .endpoint(ServerEndpoint.socketPath(socket))
        .mode(ExecutionMode.CONTROL)
        .build();

config.mode();                             // → CONTROL

Nothing else changes. The same handles answer the same questions with the same types; only the carrying differs.

The three carriers

mode how a command travels costs can stream pane output
DIRECT (default) one tmux process per command a process each time no
CONTROL one persistent tmux client one client, then reused yes
VIRTUAL a process per command, waited for on a virtual thread a process each time no

DIRECT is what the tmux binary does when a shell runs it. Nothing is held between commands, so nothing can be stale and nothing needs a session to exist.

CONTROL attaches one client and sends everything down it.

ServerConfig config = ServerConfig.builder()
        .endpoint(ServerEndpoint.socketPath(socket))
        .mode(ExecutionMode.VIRTUAL)
        .build();

config.mode();                             // → VIRTUAL

VIRTUAL is not an async mode, and nothing about it returns sooner. The call still blocks until tmux answers; what changes is which thread waits. It is for a caller stuck on a small pool of platform threads who cannot choose their own — anyone already on a virtual thread should leave it alone, because every carrier here is safe to call from one. CarrierStarvationTest runs the suite under a scheduler with exactly one carrier thread to keep that true.

Measured numbers are in the benchmark, regenerated by ./gradlew modeBenchmark.

Flipping one without editing code

A program that names no mode takes the one it is given. Nothing needs rebuilding to try another:

$ LIBTMUX_MODE=control java -jar app.jar
$ java -Dlibtmux.mode=control -jar app.jar

A value that is not a mode is refused rather than ignored, because falling back to a default would leave you believing a carrier was in force that never was — and every carrier answers the same, so nothing else would give it away.

Precedence

Highest first:

  1. Per-callserver.cmd(argv, timeout, ExecutionMode.DIRECT).
  2. In codeServerConfig.builder().mode(…), which settles it.
  3. This JVM-Dlibtmux.mode.
  4. This environmentLIBTMUX_MODE.
  5. DefaultDIRECT when nothing says otherwise.

config.mode() reports the one that won, so what is in force is a question with an answer rather than a guess about what the environment held.

List<String> argv = List.of("display-message", "-p", "#{session_name}");

// carried the way the config said
List<String> byConfig = server.cmd(argv, timeout).stdout();

// carried the way this call said
List<String> byCall = server.cmd(argv, timeout, ExecutionMode.DIRECT).stdout();

byConfig.equals(byCall);                   // → true

Both answer the same thing. That is the point, and it is also the reason the override is rarely worth reaching for: nothing a handle returns depends on which carrier answered, so this changes cost and nothing else. The one case where the carrier affects correctness — a command group — routes itself without being asked. Reach for the override when you have measured a reason, not by default.

A carrier made for an override belongs to the server that made it and is closed with it, however the server's own transport was obtained.

What is deliberately not on this switch

Batching and chaining are often listed alongside these. They are not alternatives to them, and putting them on one switch would say they were.

this is and works
server.batch() several commands in one request under any carrier
server.chain() steps acting on what the last one made under any carrier

What each buys is measured alongside the carriers in the benchmark.

Attaching takes a session

Control mode attaches to a session, so it cannot carry the command that creates the first one. Until a session exists the carrier falls back to a process, and attaches as soon as there is something to attach to.

try (Server open = Server.open(config)) {
    Session first = open.newSession("work");     // carried by a process
    first.newWindow(w -> w.named("logs"));       // carried by the control client

    open.hasSession("work");                     // → true
}

Nothing in the API marks the difference, and nothing needs to: both answer with the same types. The only trace is the process count in the benchmark.