Interactive shell

rmote shell opens an interactive shell on a host. The transport command starts a Python interpreter at the far end, rmote bootstraps its protocol into that interpreter, and the Vty tool opens a session there.

rmote shell ssh server

The module command is equivalent, with the same options and exit status:

python -m rmote shell ssh server

For a Docker container, either form opens a shell:

python -m rmote shell -- docker exec -i my-container
rmote shell -- docker exec -i my-container

Use -i without -t; rmote creates the remote terminal itself and appends python3 -qui to the transport command automatically.

The remote side needs only a Python interpreter and the standard library. No agent and no package installation are required.

The transport is a command

The common Transports guide covers command prefixes, local Python, prepared streams such as nc, and the requirements for carrying protocol bytes.

Everything after the client options is the transport command. rmote appends the interpreter and its flags, so rmote shell ssh server runs ssh server python3 -qui.

Any command works when it passes stdin and stdout through without changing the bytes:

rmote shell ssh -p 2222 -i ~/.ssh/id_ed25519 user@server
rmote shell docker exec -i my-container
rmote shell kubectl exec -i pod/my-pod --
rmote shell sudo -u postgres
rmote shell                                  # a session on the local host

Use -p / --python when the interpreter has another name or path:

rmote shell --python /usr/local/bin/python3 ssh server

Terminal or pipes

The client looks at its own descriptors. With a terminal on both stdin and stdout the session gets a pseudo terminal, which gives job control, a window size and full screen programs. With a redirected input or output the session gets pipes instead:

rmote shell ssh server                       # a terminal
rmote shell --command cat ssh server < file  # pipes

Pipes keep the bytes exactly. No carriage return is added, binary data passes through, and the end of the local input closes the input of the remote child, so a program such as cat finishes. A terminal has no half close, so there the end of input is reported with the VEOF character, normally Ctrl-D.

-T / --no-pty asks for pipes even from a terminal, for the times when exact bytes matter more than line editing:

rmote shell --no-pty --command cat ssh server

A host that cannot give a terminal reports it and names this option, so the session is still reachable there.

Choosing what runs

Without -c / --command the session starts the login shell of the remote user. Repeat the option for each argument. An argument that starts with a dash needs the = form, because the option parser would otherwise read it as an option:

rmote shell --command /bin/bash --command=-l ssh server
rmote shell --command htop ssh server

Closing the session

In raw mode every key goes to the remote session, so Ctrl-C reaches the remote program instead of the client. The escape sequence works directly after a line end, the same rule as in ssh:

Sequence

Effect

~.

Close the session. The remote child ends with it.

~~

Send one literal ~.

The remote child ends with the session. There is no saved session to come back to. -e / --escape changes the escape character, and an empty value disables the sequence:

rmote shell --escape ^ ssh server
rmote shell --escape "" ssh server

Window size and TERM

The client reports the local window size when the session opens, and again on every window change. TERM is copied from the local environment, and -t / --term overrides it.

Troubleshooting

A start that fails names its cause: the last lines the transport wrote to its stderr go into the error, so a refusal of ssh or an interpreter that cannot run appears instead of a hang. -E / --transport-stderr shows that stream live while the session runs, and -v / --debug adds protocol logs:

rmote shell --transport-stderr --debug ssh server

The exit status of rmote shell is the exit status of the remote child. A command that does not exist is reported through the session and gives status 127, as a shell does.

Sessions from Python

The tool works without the command line client. Open a session, read its output through a streaming call, and close it to get the exit status.

import asyncio
import sys

from rmote.protocol import Protocol
from rmote.tools import Vty


async def run_through_a_pipe() -> tuple[bytes, int]:
    # An empty transport starts the interpreter on the local host.
    protocol = await Protocol.from_command(python=sys.executable)
    async with protocol:
        key = await protocol(Vty.open, ["cat"], want_pty=False)
        await protocol(Vty.write, key, b"hello from the session\n")
        await protocol(Vty.end_input, key)

        output = bytearray()
        async for chunk in protocol(Vty.output, key):
            output += chunk

        return bytes(output), await protocol(Vty.close, key)


output, status = asyncio.run(run_through_a_pipe())
assert output == b"hello from the session\n"
assert status == 0

A terminal session needs the source of the controlling terminal helper, because a transferred module has no file of its own:

from rmote.tools.vty import Vty, launcher_source

key = await protocol(Vty.open, ["/bin/sh"], rows=24, cols=80, launcher=launcher_source())

Several sessions can run at once on one connection, and ordinary calls keep working while a session streams output. See streaming calls for the mechanism.

Limits

Both sides need a POSIX host. The remote session uses a pseudo terminal, and the client puts its own terminal in raw mode.

The client does not forward ports, does not forward an authentication agent and does not copy files. Use ssh and the rmote file tools for that.