terminal: Do not auto close shell terminals if they error out (#38182)

Closes https://github.com/zed-industries/zed/issues/38134

This also reduces an annoying level of shell nesting

Release Notes:

- N/A
This commit is contained in:
Lukas Wirth
2025-09-15 10:09:25 +00:00
committed by GitHub
parent 4a582504d4
commit 393d6787a3
2 changed files with 165 additions and 73 deletions
+11 -15
View File
@@ -769,13 +769,11 @@ impl RemoteClient {
}
pub fn shell(&self) -> Option<String> {
Some(self.state.as_ref()?.remote_connection()?.shell())
Some(self.remote_connection()?.shell())
}
pub fn shares_network_interface(&self) -> bool {
self.state
.as_ref()
.and_then(|state| state.remote_connection())
self.remote_connection()
.map_or(false, |connection| connection.shares_network_interface())
}
@@ -787,12 +785,8 @@ impl RemoteClient {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Some(connection) = self
.state
.as_ref()
.and_then(|state| state.remote_connection())
else {
return Err(anyhow!("no connection"));
let Some(connection) = self.remote_connection() else {
return Err(anyhow!("no ssh connection"));
};
connection.build_command(program, args, env, working_dir, port_forward)
}
@@ -803,11 +797,7 @@ impl RemoteClient {
dest_path: RemotePathBuf,
cx: &App,
) -> Task<Result<()>> {
let Some(connection) = self
.state
.as_ref()
.and_then(|state| state.remote_connection())
else {
let Some(connection) = self.remote_connection() else {
return Task::ready(Err(anyhow!("no ssh connection")));
};
connection.upload_directory(src_path, dest_path, cx)
@@ -916,6 +906,12 @@ impl RemoteClient {
.unwrap()
.unwrap()
}
fn remote_connection(&self) -> Option<Arc<dyn RemoteConnection>> {
self.state
.as_ref()
.and_then(|state| state.remote_connection())
}
}
enum ConnectionPoolEntry {
+154 -58
View File
@@ -113,64 +113,24 @@ impl RemoteConnection for SshRemoteConnection {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate> {
use std::fmt::Write as _;
let mut script = String::new();
if let Some(working_dir) = working_dir {
let working_dir =
RemotePathBuf::new(working_dir.into(), self.ssh_path_style).to_string();
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
// replace ith with something that works
const TILDE_PREFIX: &'static str = "~/";
let working_dir = if working_dir.starts_with(TILDE_PREFIX) {
let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
format!("$HOME/{working_dir}")
} else {
working_dir
};
write!(&mut script, "cd \"{working_dir}\"; ",).unwrap();
} else {
write!(&mut script, "cd; ").unwrap();
};
for (k, v) in input_env.iter() {
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
write!(&mut script, "{}={} ", k, v).unwrap();
}
}
let shell = &self.ssh_shell;
if let Some(input_program) = input_program {
let command = shlex::try_quote(&input_program)?;
script.push_str(&command);
for arg in input_args {
let arg = shlex::try_quote(&arg)?;
script.push_str(" ");
script.push_str(&arg);
}
} else {
write!(&mut script, "exec {shell} -l").unwrap();
};
let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap());
let mut args = Vec::new();
args.extend(self.socket.ssh_args());
if let Some((local_port, host, remote_port)) = port_forward {
args.push("-L".into());
args.push(format!("{local_port}:{host}:{remote_port}"));
}
args.push("-t".into());
args.push(shell_invocation);
Ok(CommandTemplate {
program: "ssh".into(),
args,
env: self.socket.envs.clone(),
})
let Self {
ssh_path_style,
socket,
ssh_shell,
..
} = self;
let env = socket.envs.clone();
build_command(
input_program,
input_args,
input_env,
working_dir,
port_forward,
env,
*ssh_path_style,
ssh_shell,
socket.ssh_args(),
)
}
fn upload_directory(
@@ -1037,3 +997,139 @@ impl SshConnectionOptions {
}
}
}
fn build_command(
input_program: Option<String>,
input_args: &[String],
input_env: &HashMap<String, String>,
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
ssh_env: HashMap<String, String>,
ssh_path_style: PathStyle,
ssh_shell: &str,
ssh_args: Vec<String>,
) -> Result<CommandTemplate> {
use std::fmt::Write as _;
let mut exec = String::from("exec env -C ");
if let Some(working_dir) = working_dir {
let working_dir = RemotePathBuf::new(working_dir.into(), ssh_path_style).to_string();
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
// replace with with something that works
const TILDE_PREFIX: &'static str = "~/";
if working_dir.starts_with(TILDE_PREFIX) {
let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
write!(exec, "\"$HOME/{working_dir}\" ",).unwrap();
} else {
write!(exec, "\"{working_dir}\" ",).unwrap();
}
} else {
write!(exec, "\"$HOME\" ").unwrap();
};
for (k, v) in input_env.iter() {
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
write!(exec, "{}={} ", k, v).unwrap();
}
}
write!(exec, "{ssh_shell} ").unwrap();
if let Some(input_program) = input_program {
let mut script = shlex::try_quote(&input_program)?.into_owned();
for arg in input_args {
let arg = shlex::try_quote(&arg)?;
script.push_str(" ");
script.push_str(&arg);
}
write!(exec, "-c {}", shlex::try_quote(&script).unwrap()).unwrap();
} else {
write!(exec, "-l").unwrap();
};
let mut args = Vec::new();
args.extend(ssh_args);
if let Some((local_port, host, remote_port)) = port_forward {
args.push("-L".into());
args.push(format!("{local_port}:{host}:{remote_port}"));
}
args.push("-t".into());
args.push(exec);
Ok(CommandTemplate {
program: "ssh".into(),
args,
env: ssh_env,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_command() -> Result<()> {
let mut input_env = HashMap::default();
input_env.insert("INPUT_VA".to_string(), "val".to_string());
let mut env = HashMap::default();
env.insert("SSH_VAR".to_string(), "ssh-val".to_string());
let command = build_command(
Some("remote_program".to_string()),
&["arg1".to_string(), "arg2".to_string()],
&input_env,
Some("~/work".to_string()),
None,
env.clone(),
PathStyle::Posix,
"/bin/fish",
vec!["-p".to_string(), "2222".to_string()],
)?;
assert_eq!(command.program, "ssh");
assert_eq!(
command.args.iter().map(String::as_str).collect::<Vec<_>>(),
[
"-p",
"2222",
"-t",
"exec env -C \"$HOME/work\" INPUT_VA=val /bin/fish -c 'remote_program arg1 arg2'"
]
);
assert_eq!(command.env, env);
let mut input_env = HashMap::default();
input_env.insert("INPUT_VA".to_string(), "val".to_string());
let mut env = HashMap::default();
env.insert("SSH_VAR".to_string(), "ssh-val".to_string());
let command = build_command(
None,
&["arg1".to_string(), "arg2".to_string()],
&input_env,
None,
Some((1, "foo".to_owned(), 2)),
env.clone(),
PathStyle::Posix,
"/bin/fish",
vec!["-p".to_string(), "2222".to_string()],
)?;
assert_eq!(command.program, "ssh");
assert_eq!(
command.args.iter().map(String::as_str).collect::<Vec<_>>(),
[
"-p",
"2222",
"-L",
"1:foo:2",
"-t",
"exec env -C \"$HOME\" INPUT_VA=val /bin/fish -l"
]
);
assert_eq!(command.env, env);
Ok(())
}
}