Fix JumpHost on Windows (#40713)

Closes #39382 

Release Notes:

- Fixed Windows specific ssh jumphost connection issues
This commit is contained in:
localcc
2025-10-22 12:04:30 +00:00
committed by GitHub
parent 98c7e018ae
commit 14b41b122f
+144 -40
View File
@@ -34,7 +34,7 @@ use util::{
pub(crate) struct SshRemoteConnection {
socket: SshSocket,
master_process: Mutex<Option<Child>>,
master_process: Mutex<Option<MasterProcess>>,
remote_binary_path: Option<Arc<RelPath>>,
ssh_platform: RemotePlatform,
ssh_path_style: PathStyle,
@@ -80,6 +80,129 @@ struct SshSocket {
_proxy: askpass::PasswordProxy,
}
struct MasterProcess {
process: Child,
}
#[cfg(not(target_os = "windows"))]
impl MasterProcess {
pub fn new(
askpass_script_path: &std::ffi::OsStr,
additional_args: Vec<String>,
socket_path: &std::path::Path,
url: &str,
) -> Result<Self> {
let args = [
"-N",
"-o",
"ControlPersist=no",
"-o",
"ControlMaster=yes",
"-o",
];
let mut master_process = util::command::new_smol_command("ssh");
master_process
.kill_on_drop(true)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("SSH_ASKPASS_REQUIRE", "force")
.env("SSH_ASKPASS", askpass_script_path)
.args(additional_args)
.args(args);
master_process.arg(format!("ControlPath={}", socket_path.display()));
let process = master_process.arg(&url).spawn()?;
Ok(MasterProcess { process })
}
pub async fn wait_connected(&mut self) -> Result<()> {
let Some(mut stdout) = self.process.stdout.take() else {
anyhow::bail!("ssh process stdout capture failed");
};
let mut output = Vec::new();
stdout.read_to_end(&mut output).await?;
Ok(())
}
}
#[cfg(target_os = "windows")]
impl MasterProcess {
const CONNECTION_ESTABLISHED_MAGIC: &str = "ZED_SSH_CONNECTION_ESTABLISHED";
pub fn new(
askpass_script_path: &std::ffi::OsStr,
additional_args: Vec<String>,
url: &str,
) -> Result<Self> {
// On Windows, `ControlMaster` and `ControlPath` are not supported:
// https://github.com/PowerShell/Win32-OpenSSH/issues/405
// https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope
//
// Using an ugly workaround to detect connection establishment
// -N doesn't work with JumpHosts as windows openssh never closes stdin in that case
let args = [
"-t",
&format!("echo '{}'; exec $0", Self::CONNECTION_ESTABLISHED_MAGIC),
];
let mut master_process = util::command::new_smol_command("ssh");
master_process
.kill_on_drop(true)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("SSH_ASKPASS_REQUIRE", "force")
.env("SSH_ASKPASS", askpass_script_path)
.args(additional_args)
.arg(url)
.args(args);
let process = master_process.spawn()?;
Ok(MasterProcess { process })
}
pub async fn wait_connected(&mut self) -> Result<()> {
use smol::io::AsyncBufReadExt;
let Some(stdout) = self.process.stdout.take() else {
anyhow::bail!("ssh process stdout capture failed");
};
let mut reader = smol::io::BufReader::new(stdout);
let mut line = String::new();
loop {
let n = reader.read_line(&mut line).await?;
if n == 0 {
anyhow::bail!("ssh process exited before connection established");
}
if line.contains(Self::CONNECTION_ESTABLISHED_MAGIC) {
return Ok(());
}
}
}
}
impl AsRef<Child> for MasterProcess {
fn as_ref(&self) -> &Child {
&self.process
}
}
impl AsMut<Child> for MasterProcess {
fn as_mut(&mut self) -> &mut Child {
&mut self.process
}
}
macro_rules! shell_script {
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
format!(
@@ -97,8 +220,8 @@ impl RemoteConnection for SshRemoteConnection {
let Some(mut process) = self.master_process.lock().take() else {
return Ok(());
};
process.kill().ok();
process.status().await?;
process.as_mut().kill().ok();
process.as_mut().status().await?;
Ok(())
}
@@ -302,45 +425,25 @@ impl SshRemoteConnection {
#[cfg(not(target_os = "windows"))]
let socket_path = temp_dir.path().join("ssh.sock");
let mut master_process = {
#[cfg(not(target_os = "windows"))]
let args = [
"-N",
"-o",
"ControlPersist=no",
"-o",
"ControlMaster=yes",
"-o",
];
// On Windows, `ControlMaster` and `ControlPath` are not supported:
// https://github.com/PowerShell/Win32-OpenSSH/issues/405
// https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope
#[cfg(target_os = "windows")]
let args = ["-N"];
let mut master_process = util::command::new_smol_command("ssh");
master_process
.kill_on_drop(true)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("SSH_ASKPASS_REQUIRE", "force")
.env("SSH_ASKPASS", askpass.script_path())
.args(connection_options.additional_args())
.args(args);
#[cfg(not(target_os = "windows"))]
master_process.arg(format!("ControlPath={}", socket_path.display()));
master_process.arg(&url).spawn()?
};
// Wait for this ssh process to close its stdout, indicating that authentication
// has completed.
let mut stdout = master_process.stdout.take().unwrap();
let mut output = Vec::new();
#[cfg(target_os = "windows")]
let mut master_process = MasterProcess::new(
askpass.script_path().as_ref(),
connection_options.additional_args(),
&url,
)?;
#[cfg(not(target_os = "windows"))]
let mut master_process = MasterProcess::new(
askpass.script_path().as_ref(),
connection_options.additional_args(),
&socket_path,
&url,
)?;
let result = select_biased! {
result = askpass.run().fuse() => {
match result {
AskPassResult::CancelledByUser => {
master_process.kill().ok();
master_process.as_mut().kill().ok();
anyhow::bail!("SSH connection canceled")
}
AskPassResult::Timedout => {
@@ -348,7 +451,7 @@ impl SshRemoteConnection {
}
}
}
_ = stdout.read_to_end(&mut output).fuse() => {
_ = master_process.wait_connected().fuse() => {
anyhow::Ok(())
}
};
@@ -357,9 +460,10 @@ impl SshRemoteConnection {
return Err(e.context("Failed to connect to host"));
}
if master_process.try_status()?.is_some() {
if master_process.as_mut().try_status()?.is_some() {
let mut output = Vec::new();
output.clear();
let mut stderr = master_process.stderr.take().unwrap();
let mut stderr = master_process.as_mut().stderr.take().unwrap();
stderr.read_to_end(&mut output).await?;
let error_message = format!(