mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(wsl): send a pane to the remote's pane socket
A remote `tty7-server` listens twice and the two dialects are not interchangeable; which socket a routed stream lands on is decided by `--pane` on the bridge command. The SSH path adds it via `RouteChannel::bridge_command`, and `LocalStdio` adds it in the client's `PaneWorkspace::route_header` because its argv is run verbatim — but `RemoteLink::wsl` always built `[binary, "--stdio"]` and dropped `setup.channel` on the floor. So a WSL pane reached the control socket, wrote its `Spawn`, and was answered with nothing: `routed connection closed after 56 up / 0 down bytes`. The workspace connected, the window opened, and the pane inside it said it could not reach the machine. `PaneWorkspace::route_header` already documents this exact failure shape; WSL was the transport that never got wired to it. WSL is the one transport that builds its own argv, so the choice belongs where the argv is built. `wsl_shell` routes its override through `bridge_command`, the same rewrite SSH applies to its shell command, so an escape hatch cannot silently lose the dialect. The argv is now a pure `wsl_link_argv`, because the difference between the two is one flag that cannot be checked anywhere a distribution is required.
This commit is contained in:
@@ -55,6 +55,7 @@ use russh::Channel;
|
||||
use russh::client::Msg;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
use super::router::RouteChannel;
|
||||
use super::ssh::ProcessStream;
|
||||
|
||||
/// One logical stream between the local daemon and a remote `tty7-server`.
|
||||
@@ -115,9 +116,21 @@ impl RemoteLink {
|
||||
/// No shell is involved, so `server` needs no quoting; the distro name is
|
||||
/// validated because it is an *option's* argument and a leading `-` would be
|
||||
/// read as another option.
|
||||
pub fn wsl(distro: &str, server: &str) -> io::Result<RemoteLink> {
|
||||
///
|
||||
/// # `channel`
|
||||
///
|
||||
/// **A pane bridge and a control bridge are different commands**, and this
|
||||
/// is the only place that can tell them apart for WSL. The remote listens
|
||||
/// twice and the dialects are not interchangeable — a pane landing on the
|
||||
/// control socket writes its `Spawn` and is answered with nothing, which is
|
||||
/// what "the workspace connects but the pane says it can't reach the
|
||||
/// machine" was. The SSH path makes the same choice in
|
||||
/// [`ssh::open_remote_link`](crate::daemon::ssh), and `LocalStdio` makes it
|
||||
/// in the client's `PaneWorkspace::route_header` because its argv is run
|
||||
/// verbatim; WSL builds its argv here, so here is where it belongs.
|
||||
pub fn wsl(distro: &str, server: &str, channel: RouteChannel) -> io::Result<RemoteLink> {
|
||||
super::install::wsl::validate_distro(distro)?;
|
||||
let args = super::install::wsl::wsl_args(distro, &[server, "--stdio"]);
|
||||
let args = super::install::wsl::wsl_args(distro, &wsl_link_argv(server, channel));
|
||||
Ok(RemoteLink::Wsl(spawn_stdio_owned(
|
||||
super::install::wsl::WSL_EXE,
|
||||
&args,
|
||||
@@ -131,9 +144,15 @@ impl RemoteLink {
|
||||
///
|
||||
/// The escape hatch for a distribution where the normal install path cannot
|
||||
/// be used; the resolved-path form above is what ships.
|
||||
pub fn wsl_shell(distro: &str, command: &str) -> io::Result<RemoteLink> {
|
||||
///
|
||||
/// `channel` reaches the command through
|
||||
/// [`RouteChannel::bridge_command`], which is the same rewrite the SSH path
|
||||
/// applies to *its* shell command — an override must not silently lose the
|
||||
/// pane dialect that [`RemoteLink::wsl`] gets right.
|
||||
pub fn wsl_shell(distro: &str, command: &str, channel: RouteChannel) -> io::Result<RemoteLink> {
|
||||
super::install::wsl::validate_distro(distro)?;
|
||||
let args = super::install::wsl::wsl_args(distro, &["sh", "-c", command]);
|
||||
let command = channel.bridge_command(command);
|
||||
let args = super::install::wsl::wsl_args(distro, &["sh", "-c", &command]);
|
||||
Ok(RemoteLink::Wsl(spawn_stdio_owned(
|
||||
super::install::wsl::WSL_EXE,
|
||||
&args,
|
||||
@@ -175,6 +194,19 @@ impl RemoteLink {
|
||||
}
|
||||
}
|
||||
|
||||
/// The command `wsl.exe` runs for a link on `channel`, as an argv.
|
||||
///
|
||||
/// Pure, because the difference between the two is one flag that decides which
|
||||
/// of the remote's two sockets the stream lands on, and it cannot be checked
|
||||
/// anywhere a distribution is required. See [`RemoteLink::wsl`].
|
||||
fn wsl_link_argv<'a>(server: &'a str, channel: RouteChannel) -> Vec<&'a str> {
|
||||
let mut argv = vec![server, "--stdio"];
|
||||
if channel == RouteChannel::Pane {
|
||||
argv.push("--pane");
|
||||
}
|
||||
argv
|
||||
}
|
||||
|
||||
fn spawn_stdio(program: &str, args: &[&str]) -> io::Result<ProcessStream> {
|
||||
let owned: Vec<String> = args.iter().map(|a| (*a).to_string()).collect();
|
||||
spawn_stdio_owned(program, &owned)
|
||||
@@ -468,6 +500,26 @@ mod tests {
|
||||
use super::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
/// **A WSL pane asks for the pane socket.** The remote listens twice and
|
||||
/// answers only the dialect it was asked for, so a pane that reaches the
|
||||
/// control socket writes its `Spawn` and is answered with nothing at all —
|
||||
/// the workspace connects, the window opens, and the pane inside it says it
|
||||
/// cannot reach the machine. Nothing in the transport reports an error,
|
||||
/// which is why this is pinned here rather than left to the one integration
|
||||
/// path that would catch it.
|
||||
#[test]
|
||||
fn only_a_pane_link_asks_for_the_pane_socket() {
|
||||
let server = "/home/me/.local/share/tty7/bin/tty7-server-26.7.6";
|
||||
assert_eq!(
|
||||
wsl_link_argv(server, RouteChannel::Control),
|
||||
vec![server, "--stdio"]
|
||||
);
|
||||
assert_eq!(
|
||||
wsl_link_argv(server, RouteChannel::Pane),
|
||||
vec![server, "--stdio", "--pane"]
|
||||
);
|
||||
}
|
||||
|
||||
/// A child process's stdio really is a duplex stream: bytes written reach
|
||||
/// the child's stdin and its stdout comes back, through the same
|
||||
/// `AsyncRead`/`AsyncWrite` the SSH variants use. This is the path the
|
||||
|
||||
@@ -1185,9 +1185,12 @@ async fn open_link(
|
||||
)
|
||||
}
|
||||
};
|
||||
// `setup.channel`, not `Control`: which of the remote's two sockets
|
||||
// this stream is for is carried by the header, and WSL is the one
|
||||
// transport that builds its own argv — see [`RemoteLink::wsl`].
|
||||
let link = match (header.server_command.as_deref(), resolved.as_deref()) {
|
||||
(Some(command), _) => RemoteLink::wsl_shell(distro, command)?,
|
||||
(None, Some(binary)) => RemoteLink::wsl(distro, binary)?,
|
||||
(Some(command), _) => RemoteLink::wsl_shell(distro, command, setup.channel)?,
|
||||
(None, Some(binary)) => RemoteLink::wsl(distro, binary, setup.channel)?,
|
||||
(None, None) => unreachable!("resolved is Some whenever there is no override"),
|
||||
};
|
||||
Ok((link, None))
|
||||
|
||||
Reference in New Issue
Block a user