Spawn remote Bash on a private channel¶
Status: accepted
Current decision¶
An eligible remote Bash execution spawns through TRAMP's direct
asynchronous process path: a local ssh (or container client) invocation
of its own, carrying cd into the working directory and the execution
environment, instead of a handler spawn multiplexed over the shared
control connection. Eligible means: the option is on (its default), the
execution is not a TTY, the target is a single-hop ssh or scp
connection, and the wrapped command fits the remote pipe buffer with
margin. Everything else -- TTY executions, oversized commands, hops,
container and other methods -- keeps the classic shared-channel spawn,
decided per record at launch. The container methods carry the
direct-async parameter too, but their per-spawn client exec allocates
a tty and prints its own notices ahead of the command; carriage
returns and interleaved client output corrupt the group marker
protocol, so those methods use the classic path.
The channel is forced per spawn rather than through a connection-local
profile. A profile flips every make-process on that host for the whole
Emacs session, including other packages' processes; forcing the TRAMP
predicate around mevedel's own spawn changes exactly one process, in both
directions -- a TTY spawn stays classic even on a connection where
something else enabled direct-async.
For ssh and scp the direct-async connection property is set to t, which
drops the method's default remote pty allocation. The pty would mangle
binary output; nothing in the stop path depends on it, because signals
reach the process group by number over the control connection, through
the group marker captured at launch -- direct-async never sets a
remote-pid, and interrupt-process never reaches the remote job, but
mevedel never used either.
The execution environment travels as an explicit env prefix inside the
group wrapper. TRAMP transfers only the difference between the current
and the top-level process-environment, which silently drops any default
the user's own environment happens to share; the prefix is deterministic.
Consequences¶
A live remote Bash no longer occupies the shared control connection for its duration. Saves, leases, publication, and stop probes stop serializing behind long-running executions, and the execution stops being the reentrancy window the transport layer's busy predicate documents as its blind spot -- that blind spot now covers only classic spawns.
Each direct-async spawn is its own ssh process: authentication must be non-interactive (an agent, a key, or connection sharing), and a spawn without socket sharing pays a handshake. The bundled TRAMP defect observed in Emacs 30.2 remains present in the inspected Emacs 31.1 sources: the spawn asks for its ssh options through a function TRAMP renamed, silently receiving none -- which loses not only ControlMaster sharing but any option routed through the same-named variable, such as the -F config a host alias needs to resolve at all. mevedel routes the intended call for each direct-async spawn's extent -- a scoped rebinding, not a global alias -- so the spawn carries the user's options and reuses the master socket while still running outside the master shell, and the adapter is inactive when the expected function already exists. The shell channel is the contention this decision removes.
The local process's exit status is the ssh client's: a remote exit 255 is indistinguishable from a transport failure. The zombie-aware group probe arbitrates the cases that matter -- group settled versus group alive -- so the ambiguity costs a probe, not a wrong outcome.
The public execution facts carry :direct-async, so a transcript records
which channel a command ran on, and the acceptance suite asserts the
private channel is actually taken for eligible spawns rather than
silently falling back.