From fa0db76811edfdcb41e629ddcecd657d5a9fe770 Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 2 Feb 2026 17:41:35 +0100 Subject: [PATCH] Fix SSH reconnection message (#48190) Closes #34531 Release Notes: - N/A --- crates/remote/src/transport/ssh.rs | 58 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index e93ed18892be6bb5b4515b67ffb902fb6244301d..811e675ca3aa011cd7713cc018797833b3f98580 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -315,7 +315,8 @@ impl RemoteConnection for SshRemoteConnection { *ssh_path_style, ssh_shell, *ssh_shell_kind, - socket.ssh_args(), + socket.ssh_command_options(), + &socket.connection_options.ssh_destination(), interactive, ) } else { @@ -329,7 +330,8 @@ impl RemoteConnection for SshRemoteConnection { *ssh_path_style, ssh_shell, *ssh_shell_kind, - socket.ssh_args(), + socket.ssh_command_options(), + &socket.connection_options.ssh_destination(), interactive, ) } @@ -340,12 +342,13 @@ impl RemoteConnection for SshRemoteConnection { forwards: Vec<(u16, String, u16)>, ) -> Result { let Self { socket, .. } = self; - let mut args = socket.ssh_args(); + let mut args = socket.ssh_command_options(); args.push("-N".into()); for (local_port, host, remote_port) in forwards { args.push("-L".into()); args.push(format!("{local_port}:{host}:{remote_port}")); } + args.push(socket.connection_options.ssh_destination()); Ok(CommandTemplate { program: "ssh".into(), args, @@ -1208,20 +1211,23 @@ impl SshSocket { cmd } - // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. - // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to - fn ssh_args(&self) -> Vec { - let mut arguments = self.connection_options.additional_args(); + // Returns the SSH command-line options (without the destination) for building commands. + // On Linux, this includes the ControlPath option to reuse the existing connection. + // Note: The destination must be added separately after all options to ensure proper + // SSH command structure: ssh [options] destination [command] + fn ssh_command_options(&self) -> Vec { + let arguments = self.connection_options.additional_args(); #[cfg(not(windows))] - arguments.extend(vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_destination(), - ]); - #[cfg(windows)] - arguments.push(self.connection_options.ssh_destination()); + let arguments = { + let mut args = arguments; + args.extend(vec![ + "-o".to_string(), + "ControlMaster=no".to_string(), + "-o".to_string(), + format!("ControlPath={}", self.socket_path.display()), + ]); + args + }; arguments } @@ -1571,7 +1577,8 @@ fn build_command_posix( ssh_path_style: PathStyle, ssh_shell: &str, ssh_shell_kind: ShellKind, - ssh_args: Vec, + ssh_options: Vec, + ssh_destination: &str, interactive: Interactive, ) -> Result { use std::fmt::Write as _; @@ -1632,7 +1639,7 @@ fn build_command_posix( }; let mut args = Vec::new(); - args.extend(ssh_args); + args.extend(ssh_options); if let Some((local_port, host, remote_port)) = port_forward { args.push("-L".into()); @@ -1648,6 +1655,8 @@ fn build_command_posix( // -T disables pseudo-TTY allocation (for non-interactive piped stdio) Interactive::No => args.push("-T".into()), } + // The destination must come after all options but before the command + args.push(ssh_destination.into()); args.push(exec); Ok(CommandTemplate { @@ -1667,7 +1676,8 @@ fn build_command_windows( ssh_path_style: PathStyle, ssh_shell: &str, _ssh_shell_kind: ShellKind, - ssh_args: Vec, + ssh_options: Vec, + ssh_destination: &str, interactive: Interactive, ) -> Result { use base64::Engine as _; @@ -1719,7 +1729,7 @@ fn build_command_windows( }; let mut args = Vec::new(); - args.extend(ssh_args); + args.extend(ssh_options); if let Some((local_port, host, remote_port)) = port_forward { args.push("-L".into()); @@ -1736,6 +1746,9 @@ fn build_command_windows( Interactive::No => args.push("-T".into()), } + // The destination must come after all options but before the command + args.push(ssh_destination.into()); + // Windows OpenSSH server incorrectly escapes the command string when the PTY is used. // The simplest way to work around this is to use a base64 encoded command, which doesn't require escaping. let utf16_bytes: Vec = exec.encode_utf16().collect(); @@ -1774,6 +1787,7 @@ mod tests { "/bin/bash", ShellKind::Posix, vec!["-o".to_string(), "ControlMaster=auto".to_string()], + "user@host", Interactive::No, )?; assert_eq!(command.program, "ssh"); @@ -1793,6 +1807,7 @@ mod tests { "/bin/fish", ShellKind::Fish, vec!["-p".to_string(), "2222".to_string()], + "user@host", Interactive::Yes, )?; @@ -1804,6 +1819,7 @@ mod tests { "2222", "-q", "-t", + "user@host", "cd \"$HOME/work\" && exec env INPUT_VA=val remote_program arg1 arg2" ] ); @@ -1825,6 +1841,7 @@ mod tests { "/bin/fish", ShellKind::Fish, vec!["-p".to_string(), "2222".to_string()], + "user@host", Interactive::Yes, )?; @@ -1838,6 +1855,7 @@ mod tests { "1:foo:2", "-q", "-t", + "user@host", "cd && exec env INPUT_VA=val /bin/fish -l" ] );