Allow passing args to ssh (#19336)
This is useful for passing a custom identity file, jump hosts, etc. Unlike with the v1 feature, we won't support `gh`/`gcloud` ssh wrappers (yet?). I think the right way of supporting those would be to let extensions provide remote projects. Closes #19118 Release Notes: - SSH remoting: restored ability to set arguments for SSH
This commit is contained in:
parent
f1d01d59ac
commit
378a2cf9d8
9 changed files with 216 additions and 89 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9126,6 +9126,7 @@ dependencies = [
|
||||||
"rpc",
|
"rpc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"shlex",
|
||||||
"smol",
|
"smol",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
|
@ -13,19 +13,19 @@ use futures::channel::oneshot;
|
||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use gpui::canvas;
|
use gpui::canvas;
|
||||||
use gpui::pulsating_between;
|
|
||||||
use gpui::AsyncWindowContext;
|
use gpui::AsyncWindowContext;
|
||||||
use gpui::ClipboardItem;
|
use gpui::ClipboardItem;
|
||||||
use gpui::Task;
|
use gpui::Task;
|
||||||
use gpui::WeakView;
|
use gpui::WeakView;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, FontWeight,
|
||||||
FocusableView, FontWeight, Model, PromptLevel, ScrollHandle, View, ViewContext,
|
Model, PromptLevel, ScrollHandle, View, ViewContext,
|
||||||
};
|
};
|
||||||
use picker::Picker;
|
use picker::Picker;
|
||||||
use project::terminals::wrap_for_ssh;
|
use project::terminals::wrap_for_ssh;
|
||||||
use project::terminals::SshCommand;
|
use project::terminals::SshCommand;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use remote::SshConnectionOptions;
|
||||||
use rpc::proto::DevServerStatus;
|
use rpc::proto::DevServerStatus;
|
||||||
use settings::update_settings_file;
|
use settings::update_settings_file;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -65,8 +65,9 @@ pub struct DevServerProjects {
|
||||||
|
|
||||||
struct CreateDevServer {
|
struct CreateDevServer {
|
||||||
address_editor: View<Editor>,
|
address_editor: View<Editor>,
|
||||||
creating: Option<Task<Option<()>>>,
|
address_error: Option<SharedString>,
|
||||||
ssh_prompt: Option<View<SshPrompt>>,
|
ssh_prompt: Option<View<SshPrompt>>,
|
||||||
|
_creating: Option<Task<Option<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateDevServer {
|
impl CreateDevServer {
|
||||||
|
@ -77,8 +78,9 @@ impl CreateDevServer {
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
address_editor,
|
address_editor,
|
||||||
creating: None,
|
address_error: None,
|
||||||
ssh_prompt: None,
|
ssh_prompt: None,
|
||||||
|
_creating: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,34 +380,22 @@ impl DevServerProjects {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_ssh_server(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
fn create_ssh_server(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||||
let host = get_text(&editor, cx);
|
let input = get_text(&editor, cx);
|
||||||
if host.is_empty() {
|
if input.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut host = host.trim_start_matches("ssh ");
|
let connection_options = match SshConnectionOptions::parse_command_line(&input) {
|
||||||
let mut username: Option<String> = None;
|
Ok(c) => c,
|
||||||
let mut port: Option<u16> = None;
|
Err(e) => {
|
||||||
|
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
if let Some((u, rest)) = host.split_once('@') {
|
address_editor: editor,
|
||||||
host = rest;
|
address_error: Some(format!("could not parse: {:?}", e).into()),
|
||||||
username = Some(u.to_string());
|
ssh_prompt: None,
|
||||||
|
_creating: None,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if let Some((rest, p)) = host.split_once(':') {
|
|
||||||
host = rest;
|
|
||||||
port = p.parse().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((rest, p)) = host.split_once(" -p") {
|
|
||||||
host = rest;
|
|
||||||
port = p.trim().parse().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection_options = remote::SshConnectionOptions {
|
|
||||||
host: host.to_string(),
|
|
||||||
username: username.clone(),
|
|
||||||
port,
|
|
||||||
password: None,
|
|
||||||
};
|
};
|
||||||
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
|
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
|
||||||
|
|
||||||
|
@ -417,6 +407,7 @@ impl DevServerProjects {
|
||||||
)
|
)
|
||||||
.prompt_err("Failed to connect", cx, |_, _| None);
|
.prompt_err("Failed to connect", cx, |_, _| None);
|
||||||
|
|
||||||
|
let address_editor = editor.clone();
|
||||||
let creating = cx.spawn(move |this, mut cx| async move {
|
let creating = cx.spawn(move |this, mut cx| async move {
|
||||||
match connection.await {
|
match connection.await {
|
||||||
Some(_) => this
|
Some(_) => this
|
||||||
|
@ -436,18 +427,31 @@ impl DevServerProjects {
|
||||||
.log_err(),
|
.log_err(),
|
||||||
None => this
|
None => this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer::new(cx));
|
address_editor.update(cx, |this, _| {
|
||||||
|
this.set_read_only(false);
|
||||||
|
});
|
||||||
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
address_editor,
|
||||||
|
address_error: None,
|
||||||
|
ssh_prompt: None,
|
||||||
|
_creating: None,
|
||||||
|
});
|
||||||
cx.notify()
|
cx.notify()
|
||||||
})
|
})
|
||||||
.log_err(),
|
.log_err(),
|
||||||
};
|
};
|
||||||
None
|
None
|
||||||
});
|
});
|
||||||
let mut state = CreateDevServer::new(cx);
|
|
||||||
state.address_editor = editor;
|
editor.update(cx, |this, _| {
|
||||||
state.ssh_prompt = Some(ssh_prompt.clone());
|
this.set_read_only(true);
|
||||||
state.creating = Some(creating);
|
});
|
||||||
self.mode = Mode::CreateDevServer(state);
|
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
address_editor: editor,
|
||||||
|
address_error: None,
|
||||||
|
ssh_prompt: Some(ssh_prompt.clone()),
|
||||||
|
_creating: Some(creating),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_server_options(
|
fn view_server_options(
|
||||||
|
@ -547,9 +551,6 @@ impl DevServerProjects {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.address_editor.update(cx, |this, _| {
|
|
||||||
this.set_read_only(true);
|
|
||||||
});
|
|
||||||
self.create_ssh_server(state.address_editor.clone(), cx);
|
self.create_ssh_server(state.address_editor.clone(), cx);
|
||||||
}
|
}
|
||||||
Mode::EditNickname(state) => {
|
Mode::EditNickname(state) => {
|
||||||
|
@ -812,6 +813,7 @@ impl DevServerProjects {
|
||||||
port: connection_options.port,
|
port: connection_options.port,
|
||||||
projects: vec![],
|
projects: vec![],
|
||||||
nickname: None,
|
nickname: None,
|
||||||
|
args: connection_options.args.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -825,10 +827,7 @@ impl DevServerProjects {
|
||||||
|
|
||||||
state.address_editor.update(cx, |editor, cx| {
|
state.address_editor.update(cx, |editor, cx| {
|
||||||
if editor.text(cx).is_empty() {
|
if editor.text(cx).is_empty() {
|
||||||
editor.set_placeholder_text(
|
editor.set_placeholder_text("ssh user@example -p 2222", cx);
|
||||||
"Enter the command you use to SSH into this server: e.g., ssh me@my.server",
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -854,27 +853,38 @@ impl DevServerProjects {
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(ssh_prompt) = ssh_prompt {
|
if let Some(ssh_prompt) = ssh_prompt {
|
||||||
this.child(h_flex().w_full().child(ssh_prompt))
|
this.child(h_flex().w_full().child(ssh_prompt))
|
||||||
|
} else if let Some(address_error) = &state.address_error {
|
||||||
|
this.child(
|
||||||
|
h_flex().p_2().w_full().gap_2().child(
|
||||||
|
Label::new(address_error.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Error),
|
||||||
|
),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
let color = Color::Muted.color(cx);
|
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_2()
|
.p_2()
|
||||||
.w_full()
|
.w_full()
|
||||||
.items_center()
|
.gap_1()
|
||||||
.justify_center()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
.child(
|
||||||
div().size_1p5().rounded_full().bg(color).with_animation(
|
Label::new(
|
||||||
"pulse-ssh-waiting-for-connection",
|
"Enter the command you use to SSH into this server.",
|
||||||
Animation::new(Duration::from_secs(2))
|
)
|
||||||
.repeat()
|
.color(Color::Muted)
|
||||||
.with_easing(pulsating_between(0.2, 0.5)),
|
.size(LabelSize::Small),
|
||||||
move |this, progress| this.bg(color.opacity(progress)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new("Waiting for connection…")
|
Button::new("learn-more", "Learn more…")
|
||||||
.size(LabelSize::Small),
|
.label_size(LabelSize::Small)
|
||||||
|
.size(ButtonSize::None)
|
||||||
|
.color(Color::Accent)
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.on_click(|_, cx| {
|
||||||
|
cx.open_url(
|
||||||
|
"https://zed.dev/docs/remote-development",
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ use picker::{
|
||||||
use rpc::proto::DevServerStatus;
|
use rpc::proto::DevServerStatus;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use ssh_connections::SshSettings;
|
pub use ssh_connections::SshSettings;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -384,11 +384,13 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let args = SshSettings::get_global(cx).args_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
|
||||||
let connection_options = SshConnectionOptions {
|
let connection_options = SshConnectionOptions {
|
||||||
host: ssh_project.host.clone(),
|
host: ssh_project.host.clone(),
|
||||||
username: ssh_project.user.clone(),
|
username: ssh_project.user.clone(),
|
||||||
port: ssh_project.port,
|
port: ssh_project.port,
|
||||||
password: None,
|
password: None,
|
||||||
|
args,
|
||||||
};
|
};
|
||||||
|
|
||||||
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
|
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
|
||||||
|
|
|
@ -32,6 +32,23 @@ impl SshSettings {
|
||||||
pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> {
|
pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> {
|
||||||
self.ssh_connections.clone().into_iter().flatten()
|
self.ssh_connections.clone().into_iter().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn args_for(
|
||||||
|
&self,
|
||||||
|
host: &str,
|
||||||
|
port: Option<u16>,
|
||||||
|
user: &Option<String>,
|
||||||
|
) -> Option<Vec<String>> {
|
||||||
|
self.ssh_connections()
|
||||||
|
.filter_map(|conn| {
|
||||||
|
if conn.host == host && &conn.username == user && conn.port == port {
|
||||||
|
Some(conn.args)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -45,6 +62,9 @@ pub struct SshConnection {
|
||||||
/// Name to use for this server in UI.
|
/// Name to use for this server in UI.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub nickname: Option<SharedString>,
|
pub nickname: Option<SharedString>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<String>,
|
||||||
}
|
}
|
||||||
impl From<SshConnection> for SshConnectionOptions {
|
impl From<SshConnection> for SshConnectionOptions {
|
||||||
fn from(val: SshConnection) -> Self {
|
fn from(val: SshConnection) -> Self {
|
||||||
|
@ -53,6 +73,7 @@ impl From<SshConnection> for SshConnectionOptions {
|
||||||
username: val.username,
|
username: val.username,
|
||||||
port: val.port,
|
port: val.port,
|
||||||
password: None,
|
password: None,
|
||||||
|
args: Some(val.args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,11 +172,9 @@ impl Render for SshPrompt {
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("PasswordPrompt")
|
.key_context("PasswordPrompt")
|
||||||
.size_full()
|
.size_full()
|
||||||
.justify_center()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_2()
|
.p_2()
|
||||||
.justify_center()
|
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.child(if self.error_message.is_some() {
|
.child(if self.error_message.is_some() {
|
||||||
Icon::new(IconName::XCircle)
|
Icon::new(IconName::XCircle)
|
||||||
|
@ -174,24 +193,19 @@ impl Render for SshPrompt {
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.ml_1()
|
|
||||||
.child(Label::new("SSH Connection").size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.when_some(self.error_message.as_ref(), |el, error| {
|
.when_some(self.error_message.as_ref(), |el, error| {
|
||||||
el.child(Label::new(format!("-{}", error)).size(LabelSize::Small))
|
el.child(Label::new(format!("{}", error)).size(LabelSize::Small))
|
||||||
})
|
})
|
||||||
.when(
|
.when(
|
||||||
self.error_message.is_none() && self.status_message.is_some(),
|
self.error_message.is_none() && self.status_message.is_some(),
|
||||||
|el| {
|
|el| {
|
||||||
el.child(
|
el.child(
|
||||||
Label::new(format!(
|
Label::new(format!(
|
||||||
"-{}",
|
"-{}…",
|
||||||
self.status_message.clone().unwrap()
|
self.status_message.clone().unwrap()
|
||||||
))
|
))
|
||||||
.size(LabelSize::Small),
|
.size(LabelSize::Small),
|
||||||
|
|
|
@ -29,6 +29,7 @@ prost.workspace = true
|
||||||
rpc = { workspace = true, features = ["gpui"] }
|
rpc = { workspace = true, features = ["gpui"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
shlex.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
|
@ -61,9 +61,89 @@ pub struct SshConnectionOptions {
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
|
pub args: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshConnectionOptions {
|
impl SshConnectionOptions {
|
||||||
|
pub fn parse_command_line(input: &str) -> Result<Self> {
|
||||||
|
let input = input.trim_start_matches("ssh ");
|
||||||
|
let mut hostname: Option<String> = None;
|
||||||
|
let mut username: Option<String> = None;
|
||||||
|
let mut port: Option<u16> = None;
|
||||||
|
let mut args = Vec::new();
|
||||||
|
|
||||||
|
// disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W
|
||||||
|
const ALLOWED_OPTS: &[&str] = &[
|
||||||
|
"-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y",
|
||||||
|
];
|
||||||
|
const ALLOWED_ARGS: &[&str] = &[
|
||||||
|
"-B", "-b", "-c", "-D", "-I", "-i", "-J", "-L", "-l", "-m", "-o", "-P", "-p", "-R",
|
||||||
|
"-w",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut tokens = shlex::split(input)
|
||||||
|
.ok_or_else(|| anyhow!("invalid input"))?
|
||||||
|
.into_iter();
|
||||||
|
|
||||||
|
'outer: while let Some(arg) = tokens.next() {
|
||||||
|
if ALLOWED_OPTS.contains(&(&arg as &str)) {
|
||||||
|
args.push(arg.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if arg == "-p" {
|
||||||
|
port = tokens.next().and_then(|arg| arg.parse().ok());
|
||||||
|
continue;
|
||||||
|
} else if let Some(p) = arg.strip_prefix("-p") {
|
||||||
|
port = p.parse().ok();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if arg == "-l" {
|
||||||
|
username = tokens.next();
|
||||||
|
continue;
|
||||||
|
} else if let Some(l) = arg.strip_prefix("-l") {
|
||||||
|
username = Some(l.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for a in ALLOWED_ARGS {
|
||||||
|
if arg == *a {
|
||||||
|
args.push(arg);
|
||||||
|
if let Some(next) = tokens.next() {
|
||||||
|
args.push(next);
|
||||||
|
}
|
||||||
|
continue 'outer;
|
||||||
|
} else if arg.starts_with(a) {
|
||||||
|
args.push(arg);
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if arg.starts_with("-") || hostname.is_some() {
|
||||||
|
anyhow::bail!("unsupported argument: {:?}", arg);
|
||||||
|
}
|
||||||
|
let mut input = &arg as &str;
|
||||||
|
if let Some((u, rest)) = input.split_once('@') {
|
||||||
|
input = rest;
|
||||||
|
username = Some(u.to_string());
|
||||||
|
}
|
||||||
|
if let Some((rest, p)) = input.split_once(':') {
|
||||||
|
input = rest;
|
||||||
|
port = p.parse().ok()
|
||||||
|
}
|
||||||
|
hostname = Some(input.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(hostname) = hostname else {
|
||||||
|
anyhow::bail!("missing hostname");
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
host: hostname.to_string(),
|
||||||
|
username: username.clone(),
|
||||||
|
port,
|
||||||
|
password: None,
|
||||||
|
args: Some(args),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ssh_url(&self) -> String {
|
pub fn ssh_url(&self) -> String {
|
||||||
let mut result = String::from("ssh://");
|
let mut result = String::from("ssh://");
|
||||||
if let Some(username) = &self.username {
|
if let Some(username) = &self.username {
|
||||||
|
@ -78,6 +158,10 @@ impl SshConnectionOptions {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn additional_args(&self) -> Option<&Vec<String>> {
|
||||||
|
self.args.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
fn scp_url(&self) -> String {
|
fn scp_url(&self) -> String {
|
||||||
if let Some(username) = &self.username {
|
if let Some(username) = &self.username {
|
||||||
format!("{}@{}", username, self.host)
|
format!("{}@{}", username, self.host)
|
||||||
|
@ -1179,6 +1263,7 @@ impl SshRemoteConnection {
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
.env("SSH_ASKPASS", &askpass_script_path)
|
.env("SSH_ASKPASS", &askpass_script_path)
|
||||||
|
.args(connection_options.additional_args().unwrap_or(&Vec::new()))
|
||||||
.args([
|
.args([
|
||||||
"-N",
|
"-N",
|
||||||
"-o",
|
"-o",
|
||||||
|
|
|
@ -11,7 +11,7 @@ use db::sqlez::{
|
||||||
};
|
};
|
||||||
use gpui::{AsyncWindowContext, Model, View, WeakView};
|
use gpui::{AsyncWindowContext, Model, View, WeakView};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use remote::{ssh_session::SshProjectId, SshConnectionOptions};
|
use remote::ssh_session::SshProjectId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -50,15 +50,6 @@ impl SerializedSshProject {
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connection_options(&self) -> SshConnectionOptions {
|
|
||||||
SshConnectionOptions {
|
|
||||||
host: self.host.clone(),
|
|
||||||
username: self.user.clone(),
|
|
||||||
port: self.port,
|
|
||||||
password: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticColumnCount for SerializedSshProject {
|
impl StaticColumnCount for SerializedSshProject {
|
||||||
|
|
|
@ -33,7 +33,7 @@ use assets::Assets;
|
||||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::project_settings::ProjectSettings;
|
use project::project_settings::ProjectSettings;
|
||||||
use recent_projects::open_ssh_project;
|
use recent_projects::{open_ssh_project, SshSettings};
|
||||||
use release_channel::{AppCommitSha, AppVersion};
|
use release_channel::{AppCommitSha, AppVersion};
|
||||||
use session::{AppSession, Session};
|
use session::{AppSession, Session};
|
||||||
use settings::{
|
use settings::{
|
||||||
|
@ -214,6 +214,7 @@ fn init_common(app_state: Arc<AppState>, cx: &mut AppContext) -> Arc<PromptBuild
|
||||||
ThemeRegistry::global(cx),
|
ThemeRegistry::global(cx),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
recent_projects::init(cx);
|
||||||
prompt_builder
|
prompt_builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +249,6 @@ fn init_ui(
|
||||||
audio::init(Assets, cx);
|
audio::init(Assets, cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
|
|
||||||
recent_projects::init(cx);
|
|
||||||
go_to_line::init(cx);
|
go_to_line::init(cx);
|
||||||
file_finder::init(cx);
|
file_finder::init(cx);
|
||||||
tab_switcher::init(cx);
|
tab_switcher::init(cx);
|
||||||
|
@ -881,18 +881,25 @@ async fn restore_or_create_workspace(
|
||||||
})?;
|
})?;
|
||||||
task.await?;
|
task.await?;
|
||||||
}
|
}
|
||||||
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
SerializedWorkspaceLocation::Ssh(ssh) => {
|
||||||
|
let args = cx
|
||||||
|
.update(|cx| {
|
||||||
|
SshSettings::get_global(cx).args_for(&ssh.host, ssh.port, &ssh.user)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
let connection_options = SshConnectionOptions {
|
let connection_options = SshConnectionOptions {
|
||||||
host: ssh_project.host.clone(),
|
args,
|
||||||
username: ssh_project.user.clone(),
|
host: ssh.host.clone(),
|
||||||
port: ssh_project.port,
|
username: ssh.user.clone(),
|
||||||
|
port: ssh.port,
|
||||||
password: None,
|
password: None,
|
||||||
};
|
};
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
cx.spawn(move |mut cx| async move {
|
cx.spawn(move |mut cx| async move {
|
||||||
recent_projects::open_ssh_project(
|
recent_projects::open_ssh_project(
|
||||||
connection_options,
|
connection_options,
|
||||||
ssh_project.paths.into_iter().map(PathBuf::from).collect(),
|
ssh.paths.into_iter().map(PathBuf::from).collect(),
|
||||||
app_state,
|
app_state,
|
||||||
workspace::OpenOptions::default(),
|
workspace::OpenOptions::default(),
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
|
|
@ -16,8 +16,9 @@ use futures::future::join_all;
|
||||||
use futures::{FutureExt, SinkExt, StreamExt};
|
use futures::{FutureExt, SinkExt, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
|
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
|
||||||
use language::{Bias, Point};
|
use language::{Bias, Point};
|
||||||
use recent_projects::open_ssh_project;
|
use recent_projects::{open_ssh_project, SshSettings};
|
||||||
use remote::SshConnectionOptions;
|
use remote::SshConnectionOptions;
|
||||||
|
use settings::Settings;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -48,7 +49,7 @@ impl OpenRequest {
|
||||||
} else if let Some(file) = url.strip_prefix("zed://file") {
|
} else if let Some(file) = url.strip_prefix("zed://file") {
|
||||||
this.parse_file_path(file)
|
this.parse_file_path(file)
|
||||||
} else if url.starts_with("ssh://") {
|
} else if url.starts_with("ssh://") {
|
||||||
this.parse_ssh_file_path(&url)?
|
this.parse_ssh_file_path(&url, cx)?
|
||||||
} else if let Some(request_path) = parse_zed_link(&url, cx) {
|
} else if let Some(request_path) = parse_zed_link(&url, cx) {
|
||||||
this.parse_request_path(request_path).log_err();
|
this.parse_request_path(request_path).log_err();
|
||||||
} else {
|
} else {
|
||||||
|
@ -65,7 +66,7 @@ impl OpenRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ssh_file_path(&mut self, file: &str) -> Result<()> {
|
fn parse_ssh_file_path(&mut self, file: &str, cx: &AppContext) -> Result<()> {
|
||||||
let url = url::Url::parse(file)?;
|
let url = url::Url::parse(file)?;
|
||||||
let host = url
|
let host = url
|
||||||
.host()
|
.host()
|
||||||
|
@ -77,11 +78,13 @@ impl OpenRequest {
|
||||||
if !self.open_paths.is_empty() {
|
if !self.open_paths.is_empty() {
|
||||||
return Err(anyhow!("cannot open both local and ssh paths"));
|
return Err(anyhow!("cannot open both local and ssh paths"));
|
||||||
}
|
}
|
||||||
|
let args = SshSettings::get_global(cx).args_for(&host, port, &username);
|
||||||
let connection = SshConnectionOptions {
|
let connection = SshConnectionOptions {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
args,
|
||||||
};
|
};
|
||||||
if let Some(ssh_connection) = &self.ssh_connection {
|
if let Some(ssh_connection) = &self.ssh_connection {
|
||||||
if *ssh_connection != connection {
|
if *ssh_connection != connection {
|
||||||
|
@ -419,12 +422,25 @@ async fn open_workspaces(
|
||||||
errored = true
|
errored = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
SerializedWorkspaceLocation::Ssh(ssh) => {
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
|
let args = cx
|
||||||
|
.update(|cx| {
|
||||||
|
SshSettings::get_global(cx).args_for(&ssh.host, ssh.port, &ssh.user)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
let connection_options = SshConnectionOptions {
|
||||||
|
args,
|
||||||
|
host: ssh.host.clone(),
|
||||||
|
username: ssh.user.clone(),
|
||||||
|
port: ssh.port,
|
||||||
|
password: None,
|
||||||
|
};
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
open_ssh_project(
|
open_ssh_project(
|
||||||
ssh_project.connection_options(),
|
connection_options,
|
||||||
ssh_project.paths.into_iter().map(PathBuf::from).collect(),
|
ssh.paths.into_iter().map(PathBuf::from).collect(),
|
||||||
app_state,
|
app_state,
|
||||||
OpenOptions::default(),
|
OpenOptions::default(),
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue