Detailed changes
@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
EditorEvent::Edited { .. } => {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
- let is_via_ssh = workspace.project().read(cx).is_via_ssh();
+ let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
workspace
.client()
@@ -1161,7 +1161,7 @@ impl Room {
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
- is_ssh_project: project.read(cx).is_via_ssh(),
+ is_ssh_project: project.read(cx).is_via_remote_server(),
});
cx.spawn(async move |this, cx| {
@@ -26,7 +26,7 @@ use project::{
debugger::session::ThreadId,
lsp_store::{FormatTrigger, LspFormatTarget},
};
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
@@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project(
.await;
// Set up project on remote FS
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
.await;
@@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches(
.await;
// Set up project on remote FS
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree("/project", serde_json::json!({ ".git":{} }))
@@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, _) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
@@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
@@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/project"), client_ssh, cx_a)
.await;
@@ -602,7 +602,7 @@ async fn test_remote_server_debugger(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -633,7 +633,7 @@ async fn test_remote_server_debugger(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
@@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
@@ -26,7 +26,7 @@ use node_runtime::NodeRuntime;
use notifications::NotificationStore;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use rpc::{
RECEIVE_TIMEOUT,
proto::{self, ChannelRole},
@@ -765,11 +765,11 @@ impl TestClient {
pub async fn build_ssh_project(
&self,
root_path: impl AsRef<Path>,
- ssh: Entity<SshRemoteClient>,
+ ssh: Entity<RemoteClient>,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = cx.update(|cx| {
- Project::ssh(
+ Project::remote(
ssh,
self.client().clone(),
self.app_state.node_runtime.clone(),
@@ -916,10 +916,11 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
- let ssh_info = project
+ let remote_shell = project
.read(cx)
- .ssh_client()
- .and_then(|it| it.read(cx).ssh_info());
+ .remote_client()
+ .as_ref()
+ .and_then(|remote| remote.read(cx).shell());
cx.spawn_in(window, async move |this, cx| {
let DebugScenario {
@@ -1003,7 +1004,7 @@ impl RunningState {
None
};
- let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell);
+ let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
let command_label = builder.command_label(&task.resolved.command_label);
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);
@@ -20074,7 +20074,7 @@ impl Editor {
let (telemetry, is_via_ssh) = {
let project = project.read(cx);
let telemetry = project.client().telemetry().clone();
- let is_via_ssh = project.is_via_ssh();
+ let is_via_ssh = project.is_via_remote_server();
(telemetry, is_via_ssh)
};
refresh_linked_ranges(self, window, cx);
@@ -20642,7 +20642,7 @@ impl Editor {
copilot_enabled,
copilot_enabled_for_language,
edit_predictions_provider,
- is_via_ssh = project.is_via_ssh(),
+ is_via_ssh = project.is_via_remote_server(),
);
} else {
telemetry::event!(
@@ -20652,7 +20652,7 @@ impl Editor {
copilot_enabled,
copilot_enabled_for_language,
edit_predictions_provider,
- is_via_ssh = project.is_via_ssh(),
+ is_via_ssh = project.is_via_remote_server(),
);
};
}
@@ -43,7 +43,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -117,7 +117,7 @@ pub struct ExtensionStore {
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
- pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>,
+ pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
@@ -270,7 +270,7 @@ impl ExtensionStore {
reload_tx,
tasks: Vec::new(),
- ssh_clients: HashMap::default(),
+ remote_clients: HashMap::default(),
ssh_registered_tx: connection_registered_tx,
};
@@ -1693,7 +1693,7 @@ impl ExtensionStore {
async fn sync_extensions_over_ssh(
this: &WeakEntity<Self>,
- client: WeakEntity<SshRemoteClient>,
+ client: WeakEntity<RemoteClient>,
cx: &mut AsyncApp,
) -> Result<()> {
let extensions = this.update(cx, |this, _cx| {
@@ -1765,8 +1765,8 @@ impl ExtensionStore {
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
let clients = this.update(cx, |this, _cx| {
- this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
- this.ssh_clients.values().cloned().collect::<Vec<_>>()
+ this.remote_clients.retain(|_k, v| v.upgrade().is_some());
+ this.remote_clients.values().cloned().collect::<Vec<_>>()
})?;
for client in clients {
@@ -1778,17 +1778,17 @@ impl ExtensionStore {
anyhow::Ok(())
}
- pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
+ pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
let connection_options = client.read(cx).connection_options();
let ssh_url = connection_options.ssh_url();
- if let Some(existing_client) = self.ssh_clients.get(&ssh_url)
+ if let Some(existing_client) = self.remote_clients.get(&ssh_url)
&& existing_client.upgrade().is_some()
{
return;
}
- self.ssh_clients.insert(ssh_url, client.downgrade());
+ self.remote_clients.insert(ssh_url, client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}
@@ -1381,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
- || ((project.is_local() || project.is_via_ssh())
+ || ((project.is_local() || project.is_via_remote_server())
&& history_item.absolute.is_some())
}),
self.currently_opened_path.as_ref(),
@@ -222,7 +222,7 @@ pub fn init(cx: &mut App) {
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
let project = workspace.project();
- if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
+ if project.read(cx).is_local() || project.read(cx).is_via_remote_server() {
log_store.update(cx, |store, cx| {
store.add_project(project, cx);
});
@@ -231,7 +231,7 @@ pub fn init(cx: &mut App) {
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
let project = workspace.project().read(cx);
- if project.is_local() || project.is_via_ssh() {
+ if project.is_local() || project.is_via_remote_server() {
let project = workspace.project().clone();
let log_store = log_store.clone();
get_or_create_tool(
@@ -321,7 +321,7 @@ impl LogStore {
.retain(|_, state| state.kind.project() != Some(&weak_project));
}),
cx.subscribe(project, |this, project, event, cx| {
- let server_kind = if project.read(cx).is_via_ssh() {
+ let server_kind = if project.read(cx).is_via_remote_server() {
LanguageServerKind::Remote {
project: project.downgrade(),
}
@@ -5102,9 +5102,9 @@ impl EventEmitter<PanelEvent> for OutlinePanel {}
impl Render for OutlinePanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let (is_local, is_via_ssh) = self
- .project
- .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
+ let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
+ (project.is_local(), project.is_via_remote_server())
+ });
let query = self.query(cx);
let pinned = self.pinned;
let settings = OutlinePanelSettings::get_global(cx);
@@ -5,11 +5,8 @@ use super::{
session::{self, Session, SessionStateEvent},
};
use crate::{
- InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
- debugger::session::SessionQuirks,
- project_settings::ProjectSettings,
- terminals::{SshCommand, wrap_for_ssh},
- worktree_store::WorktreeStore,
+ InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks,
+ project_settings::ProjectSettings, worktree_store::WorktreeStore,
};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
@@ -34,7 +31,7 @@ use http_client::HttpClient;
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
use node_runtime::NodeRuntime;
-use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs};
+use remote::RemoteClient;
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self},
@@ -68,7 +65,7 @@ pub enum DapStoreEvent {
enum DapStoreMode {
Local(LocalDapStore),
- Ssh(SshDapStore),
+ Remote(RemoteDapStore),
Collab,
}
@@ -80,8 +77,8 @@ pub struct LocalDapStore {
toolchain_store: Arc<dyn LanguageToolchainStore>,
}
-pub struct SshDapStore {
- ssh_client: Entity<SshRemoteClient>,
+pub struct RemoteDapStore {
+ remote_client: Entity<RemoteClient>,
upstream_client: AnyProtoClient,
upstream_project_id: u64,
}
@@ -147,16 +144,16 @@ impl DapStore {
Self::new(mode, breakpoint_store, worktree_store, cx)
}
- pub fn new_ssh(
+ pub fn new_remote(
project_id: u64,
- ssh_client: Entity<SshRemoteClient>,
+ remote_client: Entity<RemoteClient>,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Self {
- let mode = DapStoreMode::Ssh(SshDapStore {
- upstream_client: ssh_client.read(cx).proto_client(),
- ssh_client,
+ let mode = DapStoreMode::Remote(RemoteDapStore {
+ upstream_client: remote_client.read(cx).proto_client(),
+ remote_client,
upstream_project_id: project_id,
});
@@ -242,64 +239,51 @@ impl DapStore {
Ok(binary)
})
}
- DapStoreMode::Ssh(ssh) => {
- let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
- session_id: session_id.to_proto(),
- project_id: ssh.upstream_project_id,
- worktree_id: worktree.read(cx).id().to_proto(),
- definition: Some(definition.to_proto()),
- });
- let ssh_client = ssh.ssh_client.clone();
+ DapStoreMode::Remote(remote) => {
+ let request = remote
+ .upstream_client
+ .request(proto::GetDebugAdapterBinary {
+ session_id: session_id.to_proto(),
+ project_id: remote.upstream_project_id,
+ worktree_id: worktree.read(cx).id().to_proto(),
+ definition: Some(definition.to_proto()),
+ });
+ let remote = remote.remote_client.clone();
cx.spawn(async move |_, cx| {
let response = request.await?;
let binary = DebugAdapterBinary::from_proto(response)?;
- let (mut ssh_command, envs, path_style, ssh_shell) =
- ssh_client.read_with(cx, |ssh, _| {
- let SshInfo {
- args: SshArgs { arguments, envs },
- path_style,
- shell,
- } = ssh.ssh_info().context("SSH arguments not found")?;
- anyhow::Ok((
- SshCommand { arguments },
- envs.unwrap_or_default(),
- path_style,
- shell,
- ))
- })??;
-
- let mut connection = None;
- if let Some(c) = binary.connection {
- let local_bind_addr = Ipv4Addr::LOCALHOST;
- let port =
- dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
- ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
+ let port_forwarding;
+ let connection;
+ if let Some(c) = binary.connection {
+ let host = Ipv4Addr::LOCALHOST;
+ let port = dap::transport::TcpTransport::unused_port(host).await?;
+ port_forwarding = Some((port, c.host.to_string(), c.port));
connection = Some(TcpArguments {
port,
- host: local_bind_addr,
+ host,
timeout: c.timeout,
})
+ } else {
+ port_forwarding = None;
+ connection = None;
}
- let (program, args) = wrap_for_ssh(
- &ssh_shell,
- &ssh_command,
- binary
- .command
- .as_ref()
- .map(|command| (command, &binary.arguments)),
- binary.cwd.as_deref(),
- binary.envs,
- None,
- path_style,
- );
+ let command = remote.read_with(cx, |remote, _cx| {
+ remote.build_command(
+ binary.command,
+ &binary.arguments,
+ &binary.envs,
+ binary.cwd.map(|path| path.display().to_string()),
+ port_forwarding,
+ )
+ })??;
Ok(DebugAdapterBinary {
- command: Some(program),
- arguments: args,
- envs,
+ command: Some(command.program),
+ arguments: command.args,
+ envs: command.env,
cwd: None,
connection,
request_args: binary.request_args,
@@ -365,9 +349,9 @@ impl DapStore {
)))
}
}
- DapStoreMode::Ssh(ssh) => {
- let request = ssh.upstream_client.request(proto::RunDebugLocators {
- project_id: ssh.upstream_project_id,
+ DapStoreMode::Remote(remote) => {
+ let request = remote.upstream_client.request(proto::RunDebugLocators {
+ project_id: remote.upstream_project_id,
build_command: Some(build_command.to_proto()),
locator: locator_name.to_owned(),
});
@@ -44,7 +44,7 @@ use parking_lot::Mutex;
use postage::stream::Stream as _;
use rpc::{
AnyProtoClient, TypedEnvelope,
- proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update},
+ proto::{self, FromProto, ToProto, git_reset, split_repository_update},
};
use serde::Deserialize;
use std::{
@@ -141,14 +141,10 @@ enum GitStoreState {
project_environment: Entity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
},
- Ssh {
- upstream_client: AnyProtoClient,
- upstream_project_id: ProjectId,
- downstream: Option<(AnyProtoClient, ProjectId)>,
- },
Remote {
upstream_client: AnyProtoClient,
- upstream_project_id: ProjectId,
+ upstream_project_id: u64,
+ downstream: Option<(AnyProtoClient, ProjectId)>,
},
}
@@ -355,7 +351,7 @@ impl GitStore {
worktree_store: &Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
upstream_client: AnyProtoClient,
- project_id: ProjectId,
+ project_id: u64,
cx: &mut Context<Self>,
) -> Self {
Self::new(
@@ -364,23 +360,6 @@ impl GitStore {
GitStoreState::Remote {
upstream_client,
upstream_project_id: project_id,
- },
- cx,
- )
- }
-
- pub fn ssh(
- worktree_store: &Entity<WorktreeStore>,
- buffer_store: Entity<BufferStore>,
- upstream_client: AnyProtoClient,
- cx: &mut Context<Self>,
- ) -> Self {
- Self::new(
- worktree_store.clone(),
- buffer_store,
- GitStoreState::Ssh {
- upstream_client,
- upstream_project_id: ProjectId(SSH_PROJECT_ID),
downstream: None,
},
cx,
@@ -451,7 +430,7 @@ impl GitStore {
pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
match &mut self.state {
- GitStoreState::Ssh {
+ GitStoreState::Remote {
downstream: downstream_client,
..
} => {
@@ -527,9 +506,6 @@ impl GitStore {
}),
});
}
- GitStoreState::Remote { .. } => {
- debug_panic!("shared called on remote store");
- }
}
}
@@ -541,15 +517,12 @@ impl GitStore {
} => {
downstream_client.take();
}
- GitStoreState::Ssh {
+ GitStoreState::Remote {
downstream: downstream_client,
..
} => {
downstream_client.take();
}
- GitStoreState::Remote { .. } => {
- debug_panic!("unshared called on remote store");
- }
}
self.shared_diffs.clear();
}
@@ -1047,21 +1020,17 @@ impl GitStore {
} => downstream_client
.as_ref()
.map(|state| (state.client.clone(), state.project_id)),
- GitStoreState::Ssh {
+ GitStoreState::Remote {
downstream: downstream_client,
..
} => downstream_client.clone(),
- GitStoreState::Remote { .. } => None,
}
}
fn upstream_client(&self) -> Option<AnyProtoClient> {
match &self.state {
GitStoreState::Local { .. } => None,
- GitStoreState::Ssh {
- upstream_client, ..
- }
- | GitStoreState::Remote {
+ GitStoreState::Remote {
upstream_client, ..
} => Some(upstream_client.clone()),
}
@@ -1432,12 +1401,7 @@ impl GitStore {
cx.background_executor()
.spawn(async move { fs.git_init(&path, fallback_branch_name) })
}
- GitStoreState::Ssh {
- upstream_client,
- upstream_project_id: project_id,
- ..
- }
- | GitStoreState::Remote {
+ GitStoreState::Remote {
upstream_client,
upstream_project_id: project_id,
..
@@ -1447,7 +1411,7 @@ impl GitStore {
cx.background_executor().spawn(async move {
client
.request(proto::GitInit {
- project_id: project_id.0,
+ project_id: project_id,
abs_path: path.to_string_lossy().to_string(),
fallback_branch_name,
})
@@ -1471,13 +1435,18 @@ impl GitStore {
cx.background_executor()
.spawn(async move { fs.git_clone(&repo, &path).await })
}
- GitStoreState::Ssh {
+ GitStoreState::Remote {
upstream_client,
upstream_project_id,
..
} => {
+ if upstream_client.is_via_collab() {
+ return Task::ready(Err(anyhow!(
+ "Git Clone isn't supported for project guests"
+ )));
+ }
let request = upstream_client.request(proto::GitClone {
- project_id: upstream_project_id.0,
+ project_id: *upstream_project_id,
abs_path: path.to_string_lossy().to_string(),
remote_repo: repo,
});
@@ -1491,9 +1460,6 @@ impl GitStore {
}
})
}
- GitStoreState::Remote { .. } => {
- Task::ready(Err(anyhow!("Git Clone isn't supported for remote users")))
- }
}
}
@@ -42,9 +42,7 @@ pub use manifest_tree::ManifestTree;
use anyhow::{Context as _, Result, anyhow};
use buffer_store::{BufferStore, BufferStoreEvent};
-use client::{
- Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto,
-};
+use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto};
use clock::ReplicaId;
use dap::client::DebugAdapterClient;
@@ -89,10 +87,10 @@ use node_runtime::NodeRuntime;
use parking_lot::Mutex;
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
-use remote::{SshConnectionOptions, SshRemoteClient};
+use remote::{RemoteClient, SshConnectionOptions};
use rpc::{
AnyProtoClient, ErrorCode,
- proto::{FromProto, LanguageServerPromptResponse, SSH_PROJECT_ID, ToProto},
+ proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
};
use search::{SearchInputKind, SearchQuery, SearchResult};
use search_history::SearchHistory;
@@ -177,12 +175,12 @@ pub struct Project {
dap_store: Entity<DapStore>,
breakpoint_store: Entity<BreakpointStore>,
- client: Arc<client::Client>,
+ collab_client: Arc<client::Client>,
join_project_response_message_id: u32,
task_store: Entity<TaskStore>,
user_store: Entity<UserStore>,
fs: Arc<dyn Fs>,
- ssh_client: Option<Entity<SshRemoteClient>>,
+ remote_client: Option<Entity<RemoteClient>>,
client_state: ProjectClientState,
git_store: Entity<GitStore>,
collaborators: HashMap<proto::PeerId, Collaborator>,
@@ -1154,12 +1152,12 @@ impl Project {
active_entry: None,
snippets,
languages,
- client,
+ collab_client: client,
task_store,
user_store,
settings_observer,
fs,
- ssh_client: None,
+ remote_client: None,
breakpoint_store,
dap_store,
@@ -1183,8 +1181,8 @@ impl Project {
})
}
- pub fn ssh(
- ssh: Entity<SshRemoteClient>,
+ pub fn remote(
+ remote: Entity<RemoteClient>,
client: Arc<Client>,
node: NodeRuntime,
user_store: Entity<UserStore>,
@@ -1200,10 +1198,15 @@ impl Project {
let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
- let (ssh_proto, path_style) =
- ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
+ let (remote_proto, path_style) =
+ remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style()));
let worktree_store = cx.new(|_| {
- WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
+ WorktreeStore::remote(
+ false,
+ remote_proto.clone(),
+ REMOTE_SERVER_PROJECT_ID,
+ path_style,
+ )
});
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
@@ -1215,31 +1218,32 @@ impl Project {
let buffer_store = cx.new(|cx| {
BufferStore::remote(
worktree_store.clone(),
- ssh.read(cx).proto_client(),
- SSH_PROJECT_ID,
+ remote.read(cx).proto_client(),
+ REMOTE_SERVER_PROJECT_ID,
cx,
)
});
let image_store = cx.new(|cx| {
ImageStore::remote(
worktree_store.clone(),
- ssh.read(cx).proto_client(),
- SSH_PROJECT_ID,
+ remote.read(cx).proto_client(),
+ REMOTE_SERVER_PROJECT_ID,
cx,
)
});
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
- let toolchain_store = cx
- .new(|cx| ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx));
+ let toolchain_store = cx.new(|cx| {
+ ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx)
+ });
let task_store = cx.new(|cx| {
TaskStore::remote(
fs.clone(),
buffer_store.downgrade(),
worktree_store.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
- ssh.read(cx).proto_client(),
- SSH_PROJECT_ID,
+ remote.read(cx).proto_client(),
+ REMOTE_SERVER_PROJECT_ID,
cx,
)
});
@@ -1262,8 +1266,8 @@ impl Project {
buffer_store.clone(),
worktree_store.clone(),
languages.clone(),
- ssh_proto.clone(),
- SSH_PROJECT_ID,
+ remote_proto.clone(),
+ REMOTE_SERVER_PROJECT_ID,
fs.clone(),
cx,
)
@@ -1271,12 +1275,12 @@ impl Project {
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
let breakpoint_store =
- cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, ssh_proto.clone()));
+ cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone()));
let dap_store = cx.new(|cx| {
- DapStore::new_ssh(
- SSH_PROJECT_ID,
- ssh.clone(),
+ DapStore::new_remote(
+ REMOTE_SERVER_PROJECT_ID,
+ remote.clone(),
breakpoint_store.clone(),
worktree_store.clone(),
cx,
@@ -1284,10 +1288,16 @@ impl Project {
});
let git_store = cx.new(|cx| {
- GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
+ GitStore::remote(
+ &worktree_store,
+ buffer_store.clone(),
+ remote_proto.clone(),
+ REMOTE_SERVER_PROJECT_ID,
+ cx,
+ )
});
- cx.subscribe(&ssh, Self::on_ssh_event).detach();
+ cx.subscribe(&remote, Self::on_remote_client_event).detach();
let this = Self {
buffer_ordered_messages_tx: tx,
@@ -1306,11 +1316,13 @@ impl Project {
_subscriptions: vec![
cx.on_release(Self::release),
cx.on_app_quit(|this, cx| {
- let shutdown = this.ssh_client.take().and_then(|client| {
- client.read(cx).shutdown_processes(
- Some(proto::ShutdownRemoteServer {}),
- cx.background_executor().clone(),
- )
+ let shutdown = this.remote_client.take().and_then(|client| {
+ client.update(cx, |client, cx| {
+ client.shutdown_processes(
+ Some(proto::ShutdownRemoteServer {}),
+ cx.background_executor().clone(),
+ )
+ })
});
cx.background_executor().spawn(async move {
@@ -1323,12 +1335,12 @@ impl Project {
active_entry: None,
snippets,
languages,
- client,
+ collab_client: client,
task_store,
user_store,
settings_observer,
fs,
- ssh_client: Some(ssh.clone()),
+ remote_client: Some(remote.clone()),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@@ -1346,52 +1358,34 @@ impl Project {
agent_location: None,
};
- // ssh -> local machine handlers
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity());
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store);
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
- ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store);
-
- ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
- ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
- ssh_proto.add_entity_message_handler(Self::handle_update_project);
- ssh_proto.add_entity_message_handler(Self::handle_toast);
- ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
- ssh_proto.add_entity_message_handler(Self::handle_hide_toast);
- ssh_proto.add_entity_request_handler(Self::handle_update_buffer_from_ssh);
- BufferStore::init(&ssh_proto);
- LspStore::init(&ssh_proto);
- SettingsObserver::init(&ssh_proto);
- TaskStore::init(Some(&ssh_proto));
- ToolchainStore::init(&ssh_proto);
- DapStore::init(&ssh_proto, cx);
- GitStore::init(&ssh_proto);
+ // remote server -> local machine handlers
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.buffer_store);
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store);
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store);
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store);
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer);
+ remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store);
+
+ remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
+ remote_proto.add_entity_message_handler(Self::handle_update_worktree);
+ remote_proto.add_entity_message_handler(Self::handle_update_project);
+ remote_proto.add_entity_message_handler(Self::handle_toast);
+ remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
+ remote_proto.add_entity_message_handler(Self::handle_hide_toast);
+ remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server);
+ BufferStore::init(&remote_proto);
+ LspStore::init(&remote_proto);
+ SettingsObserver::init(&remote_proto);
+ TaskStore::init(Some(&remote_proto));
+ ToolchainStore::init(&remote_proto);
+ DapStore::init(&remote_proto, cx);
+ GitStore::init(&remote_proto);
this
})
}
- pub async fn remote(
- remote_id: u64,
- client: Arc<Client>,
- user_store: Entity<UserStore>,
- languages: Arc<LanguageRegistry>,
- fs: Arc<dyn Fs>,
- cx: AsyncApp,
- ) -> Result<Entity<Self>> {
- let project =
- Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?;
- cx.update(|cx| {
- connection_manager::Manager::global(cx).update(cx, |manager, cx| {
- manager.maintain_project_connection(&project, cx)
- })
- })?;
- Ok(project)
- }
-
pub async fn in_room(
remote_id: u64,
client: Arc<Client>,
@@ -1523,7 +1517,7 @@ impl Project {
&worktree_store,
buffer_store.clone(),
client.clone().into(),
- ProjectId(remote_id),
+ remote_id,
cx,
)
})?;
@@ -1574,11 +1568,11 @@ impl Project {
task_store,
snippets,
fs,
- ssh_client: None,
+ remote_client: None,
settings_observer: settings_observer.clone(),
client_subscriptions: Default::default(),
_subscriptions: vec![cx.on_release(Self::release)],
- client: client.clone(),
+ collab_client: client.clone(),
client_state: ProjectClientState::Remote {
sharing_has_stopped: false,
capability: Capability::ReadWrite,
@@ -1661,11 +1655,13 @@ impl Project {
}
fn release(&mut self, cx: &mut App) {
- if let Some(client) = self.ssh_client.take() {
- let shutdown = client.read(cx).shutdown_processes(
- Some(proto::ShutdownRemoteServer {}),
- cx.background_executor().clone(),
- );
+ if let Some(client) = self.remote_client.take() {
+ let shutdown = client.update(cx, |client, cx| {
+ client.shutdown_processes(
+ Some(proto::ShutdownRemoteServer {}),
+ cx.background_executor().clone(),
+ )
+ });
cx.background_spawn(async move {
if let Some(shutdown) = shutdown {
@@ -1681,7 +1677,7 @@ impl Project {
let _ = self.unshare_internal(cx);
}
ProjectClientState::Remote { remote_id, .. } => {
- let _ = self.client.send(proto::LeaveProject {
+ let _ = self.collab_client.send(proto::LeaveProject {
project_id: *remote_id,
});
self.disconnected_from_host_internal(cx);
@@ -1808,11 +1804,11 @@ impl Project {
}
pub fn client(&self) -> Arc<Client> {
- self.client.clone()
+ self.collab_client.clone()
}
- pub fn ssh_client(&self) -> Option<Entity<SshRemoteClient>> {
- self.ssh_client.clone()
+ pub fn remote_client(&self) -> Option<Entity<RemoteClient>> {
+ self.remote_client.clone()
}
pub fn user_store(&self) -> Entity<UserStore> {
@@ -1893,30 +1889,30 @@ impl Project {
if self.is_local() {
return true;
}
- if self.is_via_ssh() {
+ if self.is_via_remote_server() {
return true;
}
false
}
- pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
- self.ssh_client
+ pub fn remote_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
+ self.remote_client
.as_ref()
- .map(|ssh| ssh.read(cx).connection_state())
+ .map(|remote| remote.read(cx).connection_state())
}
- pub fn ssh_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
- self.ssh_client
+ pub fn remote_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
+ self.remote_client
.as_ref()
- .map(|ssh| ssh.read(cx).connection_options())
+ .map(|remote| remote.read(cx).connection_options())
}
pub fn replica_id(&self) -> ReplicaId {
match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id,
_ => {
- if self.ssh_client.is_some() {
+ if self.remote_client.is_some() {
1
} else {
0
@@ -2220,55 +2216,55 @@ impl Project {
);
self.client_subscriptions.extend([
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&cx.entity(), &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.worktree_store, &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.buffer_store, &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.lsp_store, &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.settings_observer, &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.dap_store, &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.breakpoint_store, &cx.to_async()),
- self.client
+ self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.git_store, &cx.to_async()),
]);
self.buffer_store.update(cx, |buffer_store, cx| {
- buffer_store.shared(project_id, self.client.clone().into(), cx)
+ buffer_store.shared(project_id, self.collab_client.clone().into(), cx)
});
self.worktree_store.update(cx, |worktree_store, cx| {
- worktree_store.shared(project_id, self.client.clone().into(), cx);
+ worktree_store.shared(project_id, self.collab_client.clone().into(), cx);
});
self.lsp_store.update(cx, |lsp_store, cx| {
- lsp_store.shared(project_id, self.client.clone().into(), cx)
+ lsp_store.shared(project_id, self.collab_client.clone().into(), cx)
});
self.breakpoint_store.update(cx, |breakpoint_store, _| {
- breakpoint_store.shared(project_id, self.client.clone().into())
+ breakpoint_store.shared(project_id, self.collab_client.clone().into())
});
self.dap_store.update(cx, |dap_store, cx| {
- dap_store.shared(project_id, self.client.clone().into(), cx);
+ dap_store.shared(project_id, self.collab_client.clone().into(), cx);
});
self.task_store.update(cx, |task_store, cx| {
- task_store.shared(project_id, self.client.clone().into(), cx);
+ task_store.shared(project_id, self.collab_client.clone().into(), cx);
});
self.settings_observer.update(cx, |settings_observer, cx| {
- settings_observer.shared(project_id, self.client.clone().into(), cx)
+ settings_observer.shared(project_id, self.collab_client.clone().into(), cx)
});
self.git_store.update(cx, |git_store, cx| {
- git_store.shared(project_id, self.client.clone().into(), cx)
+ git_store.shared(project_id, self.collab_client.clone().into(), cx)
});
self.client_state = ProjectClientState::Shared {
@@ -2293,7 +2289,7 @@ impl Project {
});
if let Some(remote_id) = self.remote_id() {
self.git_store.update(cx, |git_store, cx| {
- git_store.shared(remote_id, self.client.clone().into(), cx)
+ git_store.shared(remote_id, self.collab_client.clone().into(), cx)
});
}
cx.emit(Event::Reshared);
@@ -2370,7 +2366,7 @@ impl Project {
git_store.unshared(cx);
});
- self.client
+ self.collab_client
.send(proto::UnshareProject {
project_id: remote_id,
})
@@ -2437,15 +2433,17 @@ impl Project {
sharing_has_stopped,
..
} => *sharing_has_stopped,
- ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx),
+ ProjectClientState::Local if self.is_via_remote_server() => {
+ self.remote_client_is_disconnected(cx)
+ }
_ => false,
}
}
- fn ssh_is_disconnected(&self, cx: &App) -> bool {
- self.ssh_client
+ fn remote_client_is_disconnected(&self, cx: &App) -> bool {
+ self.remote_client
.as_ref()
- .map(|ssh| ssh.read(cx).is_disconnected())
+ .map(|remote| remote.read(cx).is_disconnected())
.unwrap_or(false)
}
@@ -2463,16 +2461,16 @@ impl Project {
pub fn is_local(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
- self.ssh_client.is_none()
+ self.remote_client.is_none()
}
ProjectClientState::Remote { .. } => false,
}
}
- pub fn is_via_ssh(&self) -> bool {
+ pub fn is_via_remote_server(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
- self.ssh_client.is_some()
+ self.remote_client.is_some()
}
ProjectClientState::Remote { .. } => false,
}
@@ -2496,7 +2494,7 @@ impl Project {
language: Option<Arc<Language>>,
cx: &mut Context<Self>,
) -> Entity<Buffer> {
- if self.is_via_collab() || self.is_via_ssh() {
+ if self.is_via_collab() || self.is_via_remote_server() {
panic!("called create_local_buffer on a remote project")
}
self.buffer_store.update(cx, |buffer_store, cx| {
@@ -2620,10 +2618,10 @@ impl Project {
) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.buffer_for_id(id, cx) {
Task::ready(Ok(buffer))
- } else if self.is_local() || self.is_via_ssh() {
+ } else if self.is_local() || self.is_via_remote_server() {
Task::ready(Err(anyhow!("buffer {id} does not exist")))
} else if let Some(project_id) = self.remote_id() {
- let request = self.client.request(proto::OpenBufferById {
+ let request = self.collab_client.request(proto::OpenBufferById {
project_id,
id: id.into(),
});
@@ -2741,7 +2739,7 @@ impl Project {
for (buffer_id, operations) in operations_by_buffer_id.drain() {
let request = this.read_with(cx, |this, _| {
let project_id = this.remote_id()?;
- Some(this.client.request(proto::UpdateBuffer {
+ Some(this.collab_client.request(proto::UpdateBuffer {
buffer_id: buffer_id.into(),
project_id,
operations,
@@ -2808,7 +2806,7 @@ impl Project {
project.read_with(cx, |project, _| {
if let Some(project_id) = project.remote_id() {
project
- .client
+ .collab_client
.send(proto::UpdateLanguageServer {
project_id,
server_name: name.map(|name| String::from(name.0)),
@@ -2846,8 +2844,8 @@ impl Project {
self.register_buffer(buffer, cx).log_err();
}
BufferStoreEvent::BufferDropped(buffer_id) => {
- if let Some(ref ssh_client) = self.ssh_client {
- ssh_client
+ if let Some(ref remote_client) = self.remote_client {
+ remote_client
.read(cx)
.proto_client()
.send(proto::CloseBuffer {
@@ -2995,16 +2993,14 @@ impl Project {
}
}
- fn on_ssh_event(
+ fn on_remote_client_event(
&mut self,
- _: Entity<SshRemoteClient>,
- event: &remote::SshRemoteEvent,
+ _: Entity<RemoteClient>,
+ event: &remote::RemoteClientEvent,
cx: &mut Context<Self>,
) {
match event {
- remote::SshRemoteEvent::Disconnected => {
- // if self.is_via_ssh() {
- // self.collaborators.clear();
+ remote::RemoteClientEvent::Disconnected => {
self.worktree_store.update(cx, |store, cx| {
store.disconnected_from_host(cx);
});
@@ -3110,8 +3106,9 @@ impl Project {
}
fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {
- if let Some(ssh) = &self.ssh_client {
- ssh.read(cx)
+ if let Some(remote) = &self.remote_client {
+ remote
+ .read(cx)
.proto_client()
.send(proto::RemoveWorktree {
worktree_id: id_to_remove.to_proto(),
@@ -3144,8 +3141,9 @@ impl Project {
} => {
let operation = language::proto::serialize_operation(operation);
- if let Some(ssh) = &self.ssh_client {
- ssh.read(cx)
+ if let Some(remote) = &self.remote_client {
+ remote
+ .read(cx)
.proto_client()
.send(proto::UpdateBuffer {
project_id: 0,
@@ -3552,16 +3550,16 @@ impl Project {
pub fn open_server_settings(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
let guard = self.retain_remotely_created_models(cx);
- let Some(ssh_client) = self.ssh_client.as_ref() else {
+ let Some(remote) = self.remote_client.as_ref() else {
return Task::ready(Err(anyhow!("not an ssh project")));
};
- let proto_client = ssh_client.read(cx).proto_client();
+ let proto_client = remote.read(cx).proto_client();
cx.spawn(async move |project, cx| {
let buffer = proto_client
.request(proto::OpenServerSettings {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
})
.await?;
@@ -3948,10 +3946,11 @@ impl Project {
) -> Receiver<Entity<Buffer>> {
let (tx, rx) = smol::channel::unbounded();
- let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
+ let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client
+ {
(ssh_client.read(cx).proto_client(), 0)
} else if let Some(remote_id) = self.remote_id() {
- (self.client.clone().into(), remote_id)
+ (self.collab_client.clone().into(), remote_id)
} else {
return rx;
};
@@ -4095,14 +4094,14 @@ impl Project {
is_dir: metadata.is_dir,
})
})
- } else if let Some(ssh_client) = self.ssh_client.as_ref() {
+ } else if let Some(ssh_client) = self.remote_client.as_ref() {
let path_style = ssh_client.read(cx).path_style();
let request_path = RemotePathBuf::from_str(path, path_style);
let request = ssh_client
.read(cx)
.proto_client()
.request(proto::GetPathMetadata {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
path: request_path.to_proto(),
});
cx.background_spawn(async move {
@@ -4202,10 +4201,10 @@ impl Project {
) -> Task<Result<Vec<DirectoryItem>>> {
if self.is_local() {
DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx)
- } else if let Some(session) = self.ssh_client.as_ref() {
+ } else if let Some(session) = self.remote_client.as_ref() {
let path_buf = PathBuf::from(query);
let request = proto::ListRemoteDirectory {
- dev_server_id: SSH_PROJECT_ID,
+ dev_server_id: REMOTE_SERVER_PROJECT_ID,
path: path_buf.to_proto(),
config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }),
};
@@ -4420,7 +4419,7 @@ impl Project {
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
- if this.is_local() || this.is_via_ssh() {
+ if this.is_local() || this.is_via_remote_server() {
this.unshare(cx)?;
} else {
this.disconnected_from_host(cx);
@@ -4629,7 +4628,7 @@ impl Project {
})?
}
- async fn handle_update_buffer_from_ssh(
+ async fn handle_update_buffer_from_remote_server(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateBuffer>,
cx: AsyncApp,
@@ -4638,7 +4637,7 @@ impl Project {
if let Some(remote_id) = this.remote_id() {
let mut payload = envelope.payload.clone();
payload.project_id = remote_id;
- cx.background_spawn(this.client.request(payload))
+ cx.background_spawn(this.collab_client.request(payload))
.detach_and_log_err(cx);
}
this.buffer_store.clone()
@@ -4652,9 +4651,9 @@ impl Project {
cx: AsyncApp,
) -> Result<proto::Ack> {
let buffer_store = this.read_with(&cx, |this, cx| {
- if let Some(ssh) = &this.ssh_client {
+ if let Some(ssh) = &this.remote_client {
let mut payload = envelope.payload.clone();
- payload.project_id = SSH_PROJECT_ID;
+ payload.project_id = REMOTE_SERVER_PROJECT_ID;
cx.background_spawn(ssh.read(cx).proto_client().request(payload))
.detach_and_log_err(cx);
}
@@ -4704,7 +4703,7 @@ impl Project {
mut cx: AsyncApp,
) -> Result<proto::SynchronizeBuffersResponse> {
let response = this.update(&mut cx, |this, cx| {
- let client = this.client.clone();
+ let client = this.collab_client.clone();
this.buffer_store.update(cx, |this, cx| {
this.handle_synchronize_buffers(envelope, cx, client)
})
@@ -4841,7 +4840,7 @@ impl Project {
}
};
- let client = self.client.clone();
+ let client = self.collab_client.clone();
cx.spawn(async move |this, cx| {
let (buffers, incomplete_buffer_ids) = this.update(cx, |this, cx| {
this.buffer_store.read(cx).buffer_version_info(cx)
@@ -2,13 +2,11 @@ use crate::{Project, ProjectPath};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
-use itertools::Itertools;
use language::LanguageName;
-use remote::{SshInfo, ssh_session::SshArgs};
+use remote::RemoteClient;
use settings::{Settings, SettingsLocation};
use smol::channel::bounded;
use std::{
- borrow::Cow,
env::{self},
path::{Path, PathBuf},
sync::Arc,
@@ -18,10 +16,7 @@ use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder,
terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
};
-use util::{
- ResultExt,
- paths::{PathStyle, RemotePathBuf},
-};
+use util::{ResultExt, paths::RemotePathBuf};
/// The directory inside a Python virtual environment that contains executables
const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") {
@@ -44,29 +39,6 @@ pub enum TerminalKind {
Task(SpawnInTerminal),
}
-/// SshCommand describes how to connect to a remote server
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SshCommand {
- pub arguments: Vec<String>,
-}
-
-impl SshCommand {
- pub fn add_port_forwarding(&mut self, local_port: u16, host: String, remote_port: u16) {
- self.arguments.push("-L".to_string());
- self.arguments
- .push(format!("{}:{}:{}", local_port, host, remote_port));
- }
-}
-
-#[derive(Debug)]
-pub struct SshDetails {
- pub host: String,
- pub ssh_command: SshCommand,
- pub envs: Option<HashMap<String, String>>,
- pub path_style: PathStyle,
- pub shell: String,
-}
-
impl Project {
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
self.active_entry()
@@ -86,28 +58,6 @@ impl Project {
}
}
- pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
- if let Some(ssh_client) = &self.ssh_client {
- let ssh_client = ssh_client.read(cx);
- if let Some(SshInfo {
- args: SshArgs { arguments, envs },
- path_style,
- shell,
- }) = ssh_client.ssh_info()
- {
- return Some(SshDetails {
- host: ssh_client.connection_options().host,
- ssh_command: SshCommand { arguments },
- envs,
- path_style,
- shell,
- });
- }
- }
-
- None
- }
-
pub fn create_terminal(
&mut self,
kind: TerminalKind,
@@ -168,14 +118,14 @@ impl Project {
TerminalSettings::get(settings_location, cx)
}
- pub fn exec_in_shell(&self, command: String, cx: &App) -> std::process::Command {
+ pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
let path = self.first_project_directory(cx);
- let ssh_details = self.ssh_details(cx);
+ let remote_client = self.remote_client.as_ref();
let settings = self.terminal_settings(&path, cx).clone();
-
- let builder =
- ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell)
- .non_interactive();
+ let remote_shell = remote_client
+ .as_ref()
+ .and_then(|remote_client| remote_client.read(cx).shell());
+ let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
let (command, args) = builder.build(Some(command), &Vec::new());
let mut env = self
@@ -185,29 +135,16 @@ impl Project {
.unwrap_or_default();
env.extend(settings.env);
- match self.ssh_details(cx) {
- Some(SshDetails {
- ssh_command,
- envs,
- path_style,
- shell,
- ..
- }) => {
- let (command, args) = wrap_for_ssh(
- &shell,
- &ssh_command,
- Some((&command, &args)),
- path.as_deref(),
- env,
- None,
- path_style,
- );
- let mut command = std::process::Command::new(command);
- command.args(args);
- if let Some(envs) = envs {
- command.envs(envs);
- }
- command
+ match remote_client {
+ Some(remote_client) => {
+ let command_template =
+ remote_client
+ .read(cx)
+ .build_command(Some(command), &args, &env, None, None)?;
+ let mut command = std::process::Command::new(command_template.program);
+ command.args(command_template.args);
+ command.envs(command_template.env);
+ Ok(command)
}
None => {
let mut command = std::process::Command::new(command);
@@ -216,7 +153,7 @@ impl Project {
if let Some(path) = path {
command.current_dir(path);
}
- command
+ Ok(command)
}
}
}
@@ -227,13 +164,13 @@ impl Project {
python_venv_directory: Option<PathBuf>,
cx: &mut Context<Self>,
) -> Result<Entity<Terminal>> {
- let this = &mut *self;
- let ssh_details = this.ssh_details(cx);
+ let is_via_remote = self.remote_client.is_some();
+
let path: Option<Arc<Path>> = match &kind {
TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
TerminalKind::Task(spawn_task) => {
if let Some(cwd) = &spawn_task.cwd {
- if ssh_details.is_some() {
+ if is_via_remote {
Some(Arc::from(cwd.as_ref()))
} else {
let cwd = cwd.to_string_lossy();
@@ -241,16 +178,14 @@ impl Project {
Some(Arc::from(Path::new(tilde_substituted.as_ref())))
}
} else {
- this.active_project_directory(cx)
+ self.active_project_directory(cx)
}
}
};
- let is_ssh_terminal = ssh_details.is_some();
-
let mut settings_location = None;
if let Some(path) = path.as_ref()
- && let Some((worktree, _)) = this.find_worktree(path, cx)
+ && let Some((worktree, _)) = self.find_worktree(path, cx)
{
settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id(),
@@ -262,7 +197,7 @@ impl Project {
let (completion_tx, completion_rx) = bounded(1);
// Start with the environment that we might have inherited from the Zed CLI.
- let mut env = this
+ let mut env = self
.environment
.read(cx)
.get_cli_environment()
@@ -271,14 +206,17 @@ impl Project {
// precedence.
env.extend(settings.env);
- let local_path = if is_ssh_terminal { None } else { path.clone() };
+ let local_path = if is_via_remote { None } else { path.clone() };
let mut python_venv_activate_command = Task::ready(None);
- let (spawn_task, shell) = match kind {
+ let remote_client = self.remote_client.clone();
+ let spawn_task;
+ let shell;
+ match kind {
TerminalKind::Shell(_) => {
if let Some(python_venv_directory) = &python_venv_directory {
- python_venv_activate_command = this.python_activate_command(
+ python_venv_activate_command = self.python_activate_command(
python_venv_directory,
&settings.detect_venv,
&settings.shell,
@@ -286,63 +224,16 @@ impl Project {
);
}
- match ssh_details {
- Some(SshDetails {
- host,
- ssh_command,
- envs,
- path_style,
- shell,
- }) => {
- log::debug!("Connecting to a remote server: {ssh_command:?}");
-
- // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
- // to properly display colors.
- // We do not have the luxury of assuming the host has it installed,
- // so we set it to a default that does not break the highlighting via ssh.
- env.entry("TERM".to_string())
- .or_insert_with(|| "xterm-256color".to_string());
-
- let (program, args) = wrap_for_ssh(
- &shell,
- &ssh_command,
- None,
- path.as_deref(),
- env,
- None,
- path_style,
- );
- env = HashMap::default();
- if let Some(envs) = envs {
- env.extend(envs);
- }
- (
- Option::<TaskState>::None,
- Shell::WithArguments {
- program,
- args,
- title_override: Some(format!("{} — Terminal", host).into()),
- },
- )
+ spawn_task = None;
+ shell = match remote_client {
+ Some(remote_client) => {
+ create_remote_shell(None, &mut env, path, remote_client, cx)?
}
- None => (None, settings.shell),
- }
+ None => settings.shell,
+ };
}
- TerminalKind::Task(spawn_task) => {
- let task_state = Some(TaskState {
- id: spawn_task.id,
- full_label: spawn_task.full_label,
- label: spawn_task.label,
- command_label: spawn_task.command_label,
- hide: spawn_task.hide,
- status: TaskStatus::Running,
- show_summary: spawn_task.show_summary,
- show_command: spawn_task.show_command,
- show_rerun: spawn_task.show_rerun,
- completion_rx,
- });
-
- env.extend(spawn_task.env);
+ TerminalKind::Task(task) => {
+ env.extend(task.env);
if let Some(venv_path) = &python_venv_directory {
env.insert(
@@ -351,41 +242,38 @@ impl Project {
);
}
- match ssh_details {
- Some(SshDetails {
- host,
- ssh_command,
- envs,
- path_style,
- shell,
- }) => {
- log::debug!("Connecting to a remote server: {ssh_command:?}");
- env.entry("TERM".to_string())
- .or_insert_with(|| "xterm-256color".to_string());
- let (program, args) = wrap_for_ssh(
- &shell,
- &ssh_command,
- spawn_task
- .command
- .as_ref()
- .map(|command| (command, &spawn_task.args)),
- path.as_deref(),
- env,
- python_venv_directory.as_deref(),
- path_style,
- );
- env = HashMap::default();
- if let Some(envs) = envs {
- env.extend(envs);
+ spawn_task = Some(TaskState {
+ id: task.id,
+ full_label: task.full_label,
+ label: task.label,
+ command_label: task.command_label,
+ hide: task.hide,
+ status: TaskStatus::Running,
+ show_summary: task.show_summary,
+ show_command: task.show_command,
+ show_rerun: task.show_rerun,
+ completion_rx,
+ });
+ shell = match remote_client {
+ Some(remote_client) => {
+ let path_style = remote_client.read(cx).path_style();
+ if let Some(venv_directory) = &python_venv_directory
+ && let Ok(str) =
+ shlex::try_quote(venv_directory.to_string_lossy().as_ref())
+ {
+ let path =
+ RemotePathBuf::new(PathBuf::from(str.to_string()), path_style)
+ .to_string();
+ env.insert("PATH".into(), format!("{}:$PATH ", path));
}
- (
- task_state,
- Shell::WithArguments {
- program,
- args,
- title_override: Some(format!("{} — Terminal", host).into()),
- },
- )
+
+ create_remote_shell(
+ task.command.as_ref().map(|command| (command, &task.args)),
+ &mut env,
+ path,
+ remote_client,
+ cx,
+ )?
}
None => {
if let Some(venv_path) = &python_venv_directory {
@@ -393,18 +281,17 @@ impl Project {
.log_err();
}
- let shell = if let Some(program) = spawn_task.command {
+ if let Some(program) = task.command {
Shell::WithArguments {
program,
- args: spawn_task.args,
+ args: task.args,
title_override: None,
}
} else {
Shell::System
- };
- (task_state, shell)
+ }
}
- }
+ };
}
};
TerminalBuilder::new(
@@ -416,7 +303,7 @@ impl Project {
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
- is_ssh_terminal,
+ is_via_remote,
cx.entity_id().as_u64(),
completion_tx,
cx,
@@ -424,7 +311,7 @@ impl Project {
.map(|builder| {
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
- this.terminals
+ self.terminals
.local_handles
.push(terminal_handle.downgrade());
@@ -442,7 +329,7 @@ impl Project {
})
.detach();
- this.activate_python_virtual_environment(
+ self.activate_python_virtual_environment(
python_venv_activate_command,
&terminal_handle,
cx,
@@ -652,62 +539,42 @@ impl Project {
}
}
-pub fn wrap_for_ssh(
- shell: &str,
- ssh_command: &SshCommand,
- command: Option<(&String, &Vec<String>)>,
- path: Option<&Path>,
- env: HashMap<String, String>,
- venv_directory: Option<&Path>,
- path_style: PathStyle,
-) -> (String, Vec<String>) {
- let to_run = if let Some((command, args)) = command {
- let command: Option<Cow<str>> = shlex::try_quote(command).ok();
- let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
- command.into_iter().chain(args).join(" ")
- } else {
- format!("exec {shell} -l")
- };
-
- let mut env_changes = String::new();
- for (k, v) in env.iter() {
- if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
- env_changes.push_str(&format!("{}={} ", k, v));
- }
- }
- if let Some(venv_directory) = venv_directory
- && let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref())
- {
- let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
- env_changes.push_str(&format!("PATH={}:$PATH ", path));
- }
-
- let commands = if let Some(path) = path {
- let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
- // shlex will wrap the command in single quotes (''), disabling ~ expansion,
- // replace ith with something that works
- let tilde_prefix = "~/";
- if path.starts_with(tilde_prefix) {
- let trimmed_path = path
- .trim_start_matches("/")
- .trim_start_matches("~")
- .trim_start_matches("/");
-
- format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
- } else {
- format!("cd \"{path}\"; {env_changes} {to_run}")
- }
- } else {
- format!("cd; {env_changes} {to_run}")
+fn create_remote_shell(
+ spawn_command: Option<(&String, &Vec<String>)>,
+ env: &mut HashMap<String, String>,
+ working_directory: Option<Arc<Path>>,
+ remote_client: Entity<RemoteClient>,
+ cx: &mut App,
+) -> Result<Shell> {
+ // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
+ // to properly display colors.
+ // We do not have the luxury of assuming the host has it installed,
+ // so we set it to a default that does not break the highlighting via ssh.
+ env.entry("TERM".to_string())
+ .or_insert_with(|| "xterm-256color".to_string());
+
+ let (program, args) = match spawn_command {
+ Some((program, args)) => (Some(program.clone()), args),
+ None => (None, &Vec::new()),
};
- let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap());
-
- let program = "ssh".to_string();
- let mut args = ssh_command.arguments.clone();
- args.push("-t".to_string());
- args.push(shell_invocation);
- (program, args)
+ let command = remote_client.read(cx).build_command(
+ program,
+ args.as_slice(),
+ env,
+ working_directory.map(|path| path.display().to_string()),
+ None,
+ )?;
+ *env = command.env;
+
+ log::debug!("Connecting to a remote server: {:?}", command.program);
+ let host = remote_client.read(cx).connection_options().host;
+
+ Ok(Shell::WithArguments {
+ program: command.program,
+ args: command.args,
+ title_override: Some(format!("{} — Terminal", host).into()),
+ })
}
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
@@ -18,7 +18,7 @@ use gpui::{
use postage::oneshot;
use rpc::{
AnyProtoClient, ErrorExt, TypedEnvelope,
- proto::{self, FromProto, SSH_PROJECT_ID, ToProto},
+ proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
};
use smol::{
channel::{Receiver, Sender},
@@ -278,7 +278,7 @@ impl WorktreeStore {
let path = RemotePathBuf::new(abs_path.into(), path_style);
let response = client
.request(proto::AddWorktree {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
path: path.to_proto(),
visible,
})
@@ -298,7 +298,7 @@ impl WorktreeStore {
let worktree = cx.update(|cx| {
Worktree::remote(
- SSH_PROJECT_ID,
+ REMOTE_SERVER_PROJECT_ID,
0,
proto::WorktreeMetadata {
id: response.worktree_id,
@@ -653,7 +653,7 @@ impl ProjectPanel {
let file_path = entry.path.clone();
let worktree_id = worktree.read(cx).id();
let entry_id = entry.id;
- let is_via_ssh = project.read(cx).is_via_ssh();
+ let is_via_ssh = project.read(cx).is_via_remote_server();
workspace
.open_path_preview(
@@ -5301,7 +5301,7 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::open_system))
.on_action(cx.listener(Self::open_in_terminal))
})
- .when(project.is_via_ssh(), |el| {
+ .when(project.is_via_remote_server(), |el| {
el.on_action(cx.listener(Self::open_in_terminal))
})
.on_mouse_down(
@@ -16,8 +16,8 @@ pub use typed_envelope::*;
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
-pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
-pub const SSH_PROJECT_ID: u64 = 0;
+pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
+pub const REMOTE_SERVER_PROJECT_ID: u64 = 0;
messages!(
(Ack, Foreground),
@@ -64,8 +64,8 @@ impl DisconnectedOverlay {
}
let handle = cx.entity().downgrade();
- let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
- let host = if let Some(ssh_connection_options) = ssh_connection_options {
+ let remote_connection_options = project.read(cx).remote_connection_options(cx);
+ let host = if let Some(ssh_connection_options) = remote_connection_options {
Host::SshRemoteProject(ssh_connection_options)
} else {
Host::RemoteProject
@@ -28,8 +28,8 @@ use paths::user_ssh_config_file;
use picker::Picker;
use project::Fs;
use project::Project;
-use remote::ssh_session::ConnectionIdentifier;
-use remote::{SshConnectionOptions, SshRemoteClient};
+use remote::remote_client::ConnectionIdentifier;
+use remote::{RemoteClient, SshConnectionOptions};
use settings::Settings;
use settings::SettingsStore;
use settings::update_settings_file;
@@ -69,7 +69,7 @@ pub struct RemoteServerProjects {
mode: Mode,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
- retained_connections: Vec<Entity<SshRemoteClient>>,
+ retained_connections: Vec<Entity<RemoteClient>>,
ssh_config_updates: Task<()>,
ssh_config_servers: BTreeSet<SharedString>,
create_new_window: bool,
@@ -597,7 +597,7 @@ impl RemoteServerProjects {
let (path_style, project) = cx.update(|_, cx| {
(
session.read(cx).path_style(),
- project::Project::ssh(
+ project::Project::remote(
session,
app_state.client.clone(),
app_state.node_runtime.clone(),
@@ -15,8 +15,9 @@ use gpui::{
use language::CursorShape;
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use release_channel::ReleaseChannel;
-use remote::ssh_session::{ConnectionIdentifier, SshPortForwardOption};
-use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
+use remote::{
+ ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption,
+};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -451,7 +452,7 @@ pub struct SshClientDelegate {
known_password: Option<String>,
}
-impl remote::SshClientDelegate for SshClientDelegate {
+impl remote::RemoteClientDelegate for SshClientDelegate {
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
let mut known_password = self.known_password.clone();
if let Some(password) = known_password.take() {
@@ -473,7 +474,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
fn download_server_binary_locally(
&self,
- platform: SshPlatform,
+ platform: RemotePlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
@@ -503,7 +504,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
fn get_download_params(
&self,
- platform: SshPlatform,
+ platform: RemotePlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
@@ -543,13 +544,13 @@ pub fn connect_over_ssh(
ui: Entity<SshPrompt>,
window: &mut Window,
cx: &mut App,
-) -> Task<Result<Option<Entity<SshRemoteClient>>>> {
+) -> Task<Result<Option<Entity<RemoteClient>>>> {
let window = window.window_handle();
let known_password = connection_options.password.clone();
let (tx, rx) = oneshot::channel();
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
- remote::SshRemoteClient::new(
+ remote::RemoteClient::ssh(
unique_identifier,
connection_options,
rx,
@@ -681,9 +682,9 @@ pub async fn open_ssh_project(
window
.update(cx, |workspace, _, cx| {
- if let Some(client) = workspace.project().read(cx).ssh_client() {
+ if let Some(client) = workspace.project().read(cx).remote_client() {
ExtensionStore::global(cx)
- .update(cx, |store, cx| store.register_ssh_client(client, cx));
+ .update(cx, |store, cx| store.register_remote_client(client, cx));
}
})
.ok();
@@ -51,6 +51,16 @@ pub async fn write_message<S: AsyncWrite + Unpin>(
Ok(())
}
+pub async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
+ stream: &mut S,
+ buffer: &mut Vec<u8>,
+) -> Result<()> {
+ let len = buffer.len() as u32;
+ stream.write_all(len.to_le_bytes().as_slice()).await?;
+ stream.write_all(buffer).await?;
+ Ok(())
+}
+
pub async fn read_message_raw<S: AsyncRead + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
@@ -1,9 +1,11 @@
pub mod json_log;
pub mod protocol;
pub mod proxy;
-pub mod ssh_session;
+pub mod remote_client;
+mod transport;
-pub use ssh_session::{
- ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform,
- SshRemoteClient, SshRemoteEvent,
+pub use remote_client::{
+ ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
+ RemotePlatform,
};
+pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
@@ -0,0 +1,1478 @@
+use crate::{
+ SshConnectionOptions, protocol::MessageId, proxy::ProxyLaunchError,
+ transport::ssh::SshRemoteConnection,
+};
+use anyhow::{Context as _, Result, anyhow};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::{
+ Future, FutureExt as _, StreamExt as _,
+ channel::{
+ mpsc::{self, Sender, UnboundedReceiver, UnboundedSender},
+ oneshot,
+ },
+ future::{BoxFuture, Shared},
+ select, select_biased,
+};
+use gpui::{
+ App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity,
+ EventEmitter, Global, SemanticVersion, Task, WeakEntity,
+};
+use parking_lot::Mutex;
+
+use release_channel::ReleaseChannel;
+use rpc::{
+ AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError,
+ proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope},
+};
+use std::{
+ collections::VecDeque,
+ fmt,
+ ops::ControlFlow,
+ path::PathBuf,
+ sync::{
+ Arc, Weak,
+ atomic::{AtomicU32, AtomicU64, Ordering::SeqCst},
+ },
+ time::{Duration, Instant},
+};
+use util::{
+ ResultExt,
+ paths::{PathStyle, RemotePathBuf},
+};
+
+#[derive(Copy, Clone, Debug)]
+pub struct RemotePlatform {
+ pub os: &'static str,
+ pub arch: &'static str,
+}
+
+#[derive(Clone, Debug)]
+pub struct CommandTemplate {
+ pub program: String,
+ pub args: Vec<String>,
+ pub env: HashMap<String, String>,
+}
+
+pub trait RemoteClientDelegate: Send + Sync {
+ fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp);
+ fn get_download_params(
+ &self,
+ platform: RemotePlatform,
+ release_channel: ReleaseChannel,
+ version: Option<SemanticVersion>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<Option<(String, String)>>>;
+ fn download_server_binary_locally(
+ &self,
+ platform: RemotePlatform,
+ release_channel: ReleaseChannel,
+ version: Option<SemanticVersion>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<PathBuf>>;
+ fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp);
+}
+
+const MAX_MISSED_HEARTBEATS: usize = 5;
+const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
+const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5);
+
+const MAX_RECONNECT_ATTEMPTS: usize = 3;
+
+enum State {
+ Connecting,
+ Connected {
+ ssh_connection: Arc<dyn RemoteConnection>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+
+ multiplex_task: Task<Result<()>>,
+ heartbeat_task: Task<Result<()>>,
+ },
+ HeartbeatMissed {
+ missed_heartbeats: usize,
+
+ ssh_connection: Arc<dyn RemoteConnection>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+
+ multiplex_task: Task<Result<()>>,
+ heartbeat_task: Task<Result<()>>,
+ },
+ Reconnecting,
+ ReconnectFailed {
+ ssh_connection: Arc<dyn RemoteConnection>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+
+ error: anyhow::Error,
+ attempts: usize,
+ },
+ ReconnectExhausted,
+ ServerNotRunning,
+}
+
+impl fmt::Display for State {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Connecting => write!(f, "connecting"),
+ Self::Connected { .. } => write!(f, "connected"),
+ Self::Reconnecting => write!(f, "reconnecting"),
+ Self::ReconnectFailed { .. } => write!(f, "reconnect failed"),
+ Self::ReconnectExhausted => write!(f, "reconnect exhausted"),
+ Self::HeartbeatMissed { .. } => write!(f, "heartbeat missed"),
+ Self::ServerNotRunning { .. } => write!(f, "server not running"),
+ }
+ }
+}
+
+impl State {
+ fn remote_connection(&self) -> Option<Arc<dyn RemoteConnection>> {
+ match self {
+ Self::Connected { ssh_connection, .. } => Some(ssh_connection.clone()),
+ Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.clone()),
+ Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.clone()),
+ _ => None,
+ }
+ }
+
+ fn can_reconnect(&self) -> bool {
+ match self {
+ Self::Connected { .. }
+ | Self::HeartbeatMissed { .. }
+ | Self::ReconnectFailed { .. } => true,
+ State::Connecting
+ | State::Reconnecting
+ | State::ReconnectExhausted
+ | State::ServerNotRunning => false,
+ }
+ }
+
+ fn is_reconnect_failed(&self) -> bool {
+ matches!(self, Self::ReconnectFailed { .. })
+ }
+
+ fn is_reconnect_exhausted(&self) -> bool {
+ matches!(self, Self::ReconnectExhausted { .. })
+ }
+
+ fn is_server_not_running(&self) -> bool {
+ matches!(self, Self::ServerNotRunning)
+ }
+
+ fn is_reconnecting(&self) -> bool {
+ matches!(self, Self::Reconnecting { .. })
+ }
+
+ fn heartbeat_recovered(self) -> Self {
+ match self {
+ Self::HeartbeatMissed {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ ..
+ } => Self::Connected {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ },
+ _ => self,
+ }
+ }
+
+ fn heartbeat_missed(self) -> Self {
+ match self {
+ Self::Connected {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ } => Self::HeartbeatMissed {
+ missed_heartbeats: 1,
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ },
+ Self::HeartbeatMissed {
+ missed_heartbeats,
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ } => Self::HeartbeatMissed {
+ missed_heartbeats: missed_heartbeats + 1,
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ },
+ _ => self,
+ }
+ }
+}
+
+/// The state of the ssh connection.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ConnectionState {
+ Connecting,
+ Connected,
+ HeartbeatMissed,
+ Reconnecting,
+ Disconnected,
+}
+
+impl From<&State> for ConnectionState {
+ fn from(value: &State) -> Self {
+ match value {
+ State::Connecting => Self::Connecting,
+ State::Connected { .. } => Self::Connected,
+ State::Reconnecting | State::ReconnectFailed { .. } => Self::Reconnecting,
+ State::HeartbeatMissed { .. } => Self::HeartbeatMissed,
+ State::ReconnectExhausted => Self::Disconnected,
+ State::ServerNotRunning => Self::Disconnected,
+ }
+ }
+}
+
+pub struct RemoteClient {
+ client: Arc<ChannelClient>,
+ unique_identifier: String,
+ connection_options: SshConnectionOptions,
+ path_style: PathStyle,
+ state: Option<State>,
+}
+
+#[derive(Debug)]
+pub enum RemoteClientEvent {
+ Disconnected,
+}
+
+impl EventEmitter<RemoteClientEvent> for RemoteClient {}
+
+// Identifies the socket on the remote server so that reconnects
+// can re-join the same project.
+pub enum ConnectionIdentifier {
+ Setup(u64),
+ Workspace(i64),
+}
+
+static NEXT_ID: AtomicU64 = AtomicU64::new(1);
+
+impl ConnectionIdentifier {
+ pub fn setup() -> Self {
+ Self::Setup(NEXT_ID.fetch_add(1, SeqCst))
+ }
+
+ // This string gets used in a socket name, and so must be relatively short.
+ // The total length of:
+ // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock
+ // Must be less than about 100 characters
+ // https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars
+ // So our strings should be at most 20 characters or so.
+ fn to_string(&self, cx: &App) -> String {
+ let identifier_prefix = match ReleaseChannel::global(cx) {
+ ReleaseChannel::Stable => "".to_string(),
+ release_channel => format!("{}-", release_channel.dev_name()),
+ };
+ match self {
+ Self::Setup(setup_id) => format!("{identifier_prefix}setup-{setup_id}"),
+ Self::Workspace(workspace_id) => {
+ format!("{identifier_prefix}workspace-{workspace_id}",)
+ }
+ }
+ }
+}
+
+impl RemoteClient {
+ pub fn ssh(
+ unique_identifier: ConnectionIdentifier,
+ connection_options: SshConnectionOptions,
+ cancellation: oneshot::Receiver<()>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut App,
+ ) -> Task<Result<Option<Entity<Self>>>> {
+ let unique_identifier = unique_identifier.to_string(cx);
+ cx.spawn(async move |cx| {
+ let success = Box::pin(async move {
+ let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
+ let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
+ let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
+
+ let client =
+ cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
+
+ let ssh_connection = cx
+ .update(|cx| {
+ cx.update_default_global(|pool: &mut ConnectionPool, cx| {
+ pool.connect(connection_options.clone(), &delegate, cx)
+ })
+ })?
+ .await
+ .map_err(|e| e.cloned())?;
+
+ let path_style = ssh_connection.path_style();
+ let this = cx.new(|_| Self {
+ client: client.clone(),
+ unique_identifier: unique_identifier.clone(),
+ connection_options,
+ path_style,
+ state: Some(State::Connecting),
+ })?;
+
+ let io_task = ssh_connection.start_proxy(
+ unique_identifier,
+ false,
+ incoming_tx,
+ outgoing_rx,
+ connection_activity_tx,
+ delegate.clone(),
+ cx,
+ );
+
+ let multiplex_task = Self::monitor(this.downgrade(), io_task, cx);
+
+ if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
+ log::error!("failed to establish connection: {}", error);
+ return Err(error);
+ }
+
+ let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx);
+
+ this.update(cx, |this, _| {
+ this.state = Some(State::Connected {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ });
+ })?;
+
+ Ok(Some(this))
+ });
+
+ select! {
+ _ = cancellation.fuse() => {
+ Ok(None)
+ }
+ result = success.fuse() => result
+ }
+ })
+ }
+
+ pub fn proto_client_from_channels(
+ incoming_rx: mpsc::UnboundedReceiver<Envelope>,
+ outgoing_tx: mpsc::UnboundedSender<Envelope>,
+ cx: &App,
+ name: &'static str,
+ ) -> AnyProtoClient {
+ ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into()
+ }
+
+ pub fn shutdown_processes<T: RequestMessage>(
+ &mut self,
+ shutdown_request: Option<T>,
+ executor: BackgroundExecutor,
+ ) -> Option<impl Future<Output = ()> + use<T>> {
+ let state = self.state.take()?;
+ log::info!("shutting down ssh processes");
+
+ let State::Connected {
+ multiplex_task,
+ heartbeat_task,
+ ssh_connection,
+ delegate,
+ } = state
+ else {
+ return None;
+ };
+
+ let client = self.client.clone();
+
+ Some(async move {
+ if let Some(shutdown_request) = shutdown_request {
+ client.send(shutdown_request).log_err();
+ // We wait 50ms instead of waiting for a response, because
+ // waiting for a response would require us to wait on the main thread
+ // which we want to avoid in an `on_app_quit` callback.
+ executor.timer(Duration::from_millis(50)).await;
+ }
+
+ // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a
+ // child of master_process.
+ drop(multiplex_task);
+ // Now drop the rest of state, which kills master process.
+ drop(heartbeat_task);
+ drop(ssh_connection);
+ drop(delegate);
+ })
+ }
+
+ fn reconnect(&mut self, cx: &mut Context<Self>) -> Result<()> {
+ let can_reconnect = self
+ .state
+ .as_ref()
+ .map(|state| state.can_reconnect())
+ .unwrap_or(false);
+ if !can_reconnect {
+ log::info!("aborting reconnect, because not in state that allows reconnecting");
+ let error = if let Some(state) = self.state.as_ref() {
+ format!("invalid state, cannot reconnect while in state {state}")
+ } else {
+ "no state set".to_string()
+ };
+ anyhow::bail!(error);
+ }
+
+ let state = self.state.take().unwrap();
+ let (attempts, ssh_connection, delegate) = match state {
+ State::Connected {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ }
+ | State::HeartbeatMissed {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task,
+ ..
+ } => {
+ drop(multiplex_task);
+ drop(heartbeat_task);
+ (0, ssh_connection, delegate)
+ }
+ State::ReconnectFailed {
+ attempts,
+ ssh_connection,
+ delegate,
+ ..
+ } => (attempts, ssh_connection, delegate),
+ State::Connecting
+ | State::Reconnecting
+ | State::ReconnectExhausted
+ | State::ServerNotRunning => unreachable!(),
+ };
+
+ let attempts = attempts + 1;
+ if attempts > MAX_RECONNECT_ATTEMPTS {
+ log::error!(
+ "Failed to reconnect to after {} attempts, giving up",
+ MAX_RECONNECT_ATTEMPTS
+ );
+ self.set_state(State::ReconnectExhausted, cx);
+ return Ok(());
+ }
+
+ self.set_state(State::Reconnecting, cx);
+
+ log::info!("Trying to reconnect to ssh server... Attempt {}", attempts);
+
+ let unique_identifier = self.unique_identifier.clone();
+ let client = self.client.clone();
+ let reconnect_task = cx.spawn(async move |this, cx| {
+ macro_rules! failed {
+ ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => {
+ return State::ReconnectFailed {
+ error: anyhow!($error),
+ attempts: $attempts,
+ ssh_connection: $ssh_connection,
+ delegate: $delegate,
+ };
+ };
+ }
+
+ if let Err(error) = ssh_connection
+ .kill()
+ .await
+ .context("Failed to kill ssh process")
+ {
+ failed!(error, attempts, ssh_connection, delegate);
+ };
+
+ let connection_options = ssh_connection.connection_options();
+
+ let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
+ let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
+ let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
+
+ let (ssh_connection, io_task) = match async {
+ let ssh_connection = cx
+ .update_global(|pool: &mut ConnectionPool, cx| {
+ pool.connect(connection_options, &delegate, cx)
+ })?
+ .await
+ .map_err(|error| error.cloned())?;
+
+ let io_task = ssh_connection.start_proxy(
+ unique_identifier,
+ true,
+ incoming_tx,
+ outgoing_rx,
+ connection_activity_tx,
+ delegate.clone(),
+ cx,
+ );
+ anyhow::Ok((ssh_connection, io_task))
+ }
+ .await
+ {
+ Ok((ssh_connection, io_task)) => (ssh_connection, io_task),
+ Err(error) => {
+ failed!(error, attempts, ssh_connection, delegate);
+ }
+ };
+
+ let multiplex_task = Self::monitor(this.clone(), io_task, cx);
+ client.reconnect(incoming_rx, outgoing_tx, cx);
+
+ if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
+ failed!(error, attempts, ssh_connection, delegate);
+ };
+
+ State::Connected {
+ ssh_connection,
+ delegate,
+ multiplex_task,
+ heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx),
+ }
+ });
+
+ cx.spawn(async move |this, cx| {
+ let new_state = reconnect_task.await;
+ this.update(cx, |this, cx| {
+ this.try_set_state(cx, |old_state| {
+ if old_state.is_reconnecting() {
+ match &new_state {
+ State::Connecting
+ | State::Reconnecting
+ | State::HeartbeatMissed { .. }
+ | State::ServerNotRunning => {}
+ State::Connected { .. } => {
+ log::info!("Successfully reconnected");
+ }
+ State::ReconnectFailed {
+ error, attempts, ..
+ } => {
+ log::error!(
+ "Reconnect attempt {} failed: {:?}. Starting new attempt...",
+ attempts,
+ error
+ );
+ }
+ State::ReconnectExhausted => {
+ log::error!("Reconnect attempt failed and all attempts exhausted");
+ }
+ }
+ Some(new_state)
+ } else {
+ None
+ }
+ });
+
+ if this.state_is(State::is_reconnect_failed) {
+ this.reconnect(cx)
+ } else if this.state_is(State::is_reconnect_exhausted) {
+ Ok(())
+ } else {
+ log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
+ Ok(())
+ }
+ })
+ })
+ .detach_and_log_err(cx);
+
+ Ok(())
+ }
+
+ fn heartbeat(
+ this: WeakEntity<Self>,
+ mut connection_activity_rx: mpsc::Receiver<()>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<()>> {
+ let Ok(client) = this.read_with(cx, |this, _| this.client.clone()) else {
+ return Task::ready(Err(anyhow!("SshRemoteClient lost")));
+ };
+
+ cx.spawn(async move |cx| {
+ let mut missed_heartbeats = 0;
+
+ let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse();
+ futures::pin_mut!(keepalive_timer);
+
+ loop {
+ select_biased! {
+ result = connection_activity_rx.next().fuse() => {
+ if result.is_none() {
+ log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping.");
+ return Ok(());
+ }
+
+ if missed_heartbeats != 0 {
+ missed_heartbeats = 0;
+ let _ =this.update(cx, |this, cx| {
+ this.handle_heartbeat_result(missed_heartbeats, cx)
+ })?;
+ }
+ }
+ _ = keepalive_timer => {
+ log::debug!("Sending heartbeat to server...");
+
+ let result = select_biased! {
+ _ = connection_activity_rx.next().fuse() => {
+ Ok(())
+ }
+ ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => {
+ ping_result
+ }
+ };
+
+ if result.is_err() {
+ missed_heartbeats += 1;
+ log::warn!(
+ "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.",
+ HEARTBEAT_TIMEOUT,
+ missed_heartbeats,
+ MAX_MISSED_HEARTBEATS
+ );
+ } else if missed_heartbeats != 0 {
+ missed_heartbeats = 0;
+ } else {
+ continue;
+ }
+
+ let result = this.update(cx, |this, cx| {
+ this.handle_heartbeat_result(missed_heartbeats, cx)
+ })?;
+ if result.is_break() {
+ return Ok(());
+ }
+ }
+ }
+
+ keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
+ }
+ })
+ }
+
+ fn handle_heartbeat_result(
+ &mut self,
+ missed_heartbeats: usize,
+ cx: &mut Context<Self>,
+ ) -> ControlFlow<()> {
+ let state = self.state.take().unwrap();
+ let next_state = if missed_heartbeats > 0 {
+ state.heartbeat_missed()
+ } else {
+ state.heartbeat_recovered()
+ };
+
+ self.set_state(next_state, cx);
+
+ if missed_heartbeats >= MAX_MISSED_HEARTBEATS {
+ log::error!(
+ "Missed last {} heartbeats. Reconnecting...",
+ missed_heartbeats
+ );
+
+ self.reconnect(cx)
+ .context("failed to start reconnect process after missing heartbeats")
+ .log_err();
+ ControlFlow::Break(())
+ } else {
+ ControlFlow::Continue(())
+ }
+ }
+
+ fn monitor(
+ this: WeakEntity<Self>,
+ io_task: Task<Result<i32>>,
+ cx: &AsyncApp,
+ ) -> Task<Result<()>> {
+ cx.spawn(async move |cx| {
+ let result = io_task.await;
+
+ match result {
+ Ok(exit_code) => {
+ if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) {
+ match error {
+ ProxyLaunchError::ServerNotRunning => {
+ log::error!("failed to reconnect because server is not running");
+ this.update(cx, |this, cx| {
+ this.set_state(State::ServerNotRunning, cx);
+ })?;
+ }
+ }
+ } else if exit_code > 0 {
+ log::error!("proxy process terminated unexpectedly");
+ this.update(cx, |this, cx| {
+ this.reconnect(cx).ok();
+ })?;
+ }
+ }
+ Err(error) => {
+ log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
+ this.update(cx, |this, cx| {
+ this.reconnect(cx).ok();
+ })?;
+ }
+ }
+
+ Ok(())
+ })
+ }
+
+ fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool {
+ self.state.as_ref().is_some_and(check)
+ }
+
+ fn try_set_state(&mut self, cx: &mut Context<Self>, map: impl FnOnce(&State) -> Option<State>) {
+ let new_state = self.state.as_ref().and_then(map);
+ if let Some(new_state) = new_state {
+ self.state.replace(new_state);
+ cx.notify();
+ }
+ }
+
+ fn set_state(&mut self, state: State, cx: &mut Context<Self>) {
+ log::info!("setting state to '{}'", &state);
+
+ let is_reconnect_exhausted = state.is_reconnect_exhausted();
+ let is_server_not_running = state.is_server_not_running();
+ self.state.replace(state);
+
+ if is_reconnect_exhausted || is_server_not_running {
+ cx.emit(RemoteClientEvent::Disconnected);
+ }
+ cx.notify();
+ }
+
+ pub fn shell(&self) -> Option<String> {
+ Some(self.state.as_ref()?.remote_connection()?.shell())
+ }
+
+ pub fn build_command(
+ &self,
+ program: Option<String>,
+ args: &[String],
+ env: &HashMap<String, String>,
+ 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"));
+ };
+ connection.build_command(program, args, env, working_dir, port_forward)
+ }
+
+ pub fn upload_directory(
+ &self,
+ src_path: PathBuf,
+ dest_path: RemotePathBuf,
+ cx: &App,
+ ) -> Task<Result<()>> {
+ let Some(connection) = self
+ .state
+ .as_ref()
+ .and_then(|state| state.remote_connection())
+ else {
+ return Task::ready(Err(anyhow!("no ssh connection")));
+ };
+ connection.upload_directory(src_path, dest_path, cx)
+ }
+
+ pub fn proto_client(&self) -> AnyProtoClient {
+ self.client.clone().into()
+ }
+
+ pub fn host(&self) -> String {
+ self.connection_options.host.clone()
+ }
+
+ pub fn connection_options(&self) -> SshConnectionOptions {
+ self.connection_options.clone()
+ }
+
+ pub fn connection_state(&self) -> ConnectionState {
+ self.state
+ .as_ref()
+ .map(ConnectionState::from)
+ .unwrap_or(ConnectionState::Disconnected)
+ }
+
+ pub fn is_disconnected(&self) -> bool {
+ self.connection_state() == ConnectionState::Disconnected
+ }
+
+ pub fn path_style(&self) -> PathStyle {
+ self.path_style
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> {
+ let opts = self.connection_options();
+ client_cx.spawn(async move |cx| {
+ let connection = cx
+ .update_global(|c: &mut ConnectionPool, _| {
+ if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) {
+ c.clone()
+ } else {
+ panic!("missing test connection")
+ }
+ })
+ .unwrap()
+ .await
+ .unwrap();
+
+ connection.simulate_disconnect(cx);
+ })
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn fake_server(
+ client_cx: &mut gpui::TestAppContext,
+ server_cx: &mut gpui::TestAppContext,
+ ) -> (SshConnectionOptions, AnyProtoClient) {
+ let port = client_cx
+ .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
+ let opts = SshConnectionOptions {
+ host: "<fake>".to_string(),
+ port: Some(port),
+ ..Default::default()
+ };
+ let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
+ let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
+ let server_client =
+ server_cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server"));
+ let connection: Arc<dyn RemoteConnection> = Arc::new(fake::FakeRemoteConnection {
+ connection_options: opts.clone(),
+ server_cx: fake::SendableCx::new(server_cx),
+ server_channel: server_client.clone(),
+ });
+
+ client_cx.update(|cx| {
+ cx.update_default_global(|c: &mut ConnectionPool, cx| {
+ c.connections.insert(
+ opts.clone(),
+ ConnectionPoolEntry::Connecting(
+ cx.background_spawn({
+ let connection = connection.clone();
+ async move { Ok(connection.clone()) }
+ })
+ .shared(),
+ ),
+ );
+ })
+ });
+
+ (opts, server_client.into())
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub async fn fake_client(
+ opts: SshConnectionOptions,
+ client_cx: &mut gpui::TestAppContext,
+ ) -> Entity<Self> {
+ let (_tx, rx) = oneshot::channel();
+ client_cx
+ .update(|cx| {
+ Self::ssh(
+ ConnectionIdentifier::setup(),
+ opts,
+ rx,
+ Arc::new(fake::Delegate),
+ cx,
+ )
+ })
+ .await
+ .unwrap()
+ .unwrap()
+ }
+}
+
+enum ConnectionPoolEntry {
+ Connecting(Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>>),
+ Connected(Weak<dyn RemoteConnection>),
+}
+
+#[derive(Default)]
+struct ConnectionPool {
+ connections: HashMap<SshConnectionOptions, ConnectionPoolEntry>,
+}
+
+impl Global for ConnectionPool {}
+
+impl ConnectionPool {
+ pub fn connect(
+ &mut self,
+ opts: SshConnectionOptions,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ cx: &mut App,
+ ) -> Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>> {
+ let connection = self.connections.get(&opts);
+ match connection {
+ Some(ConnectionPoolEntry::Connecting(task)) => {
+ let delegate = delegate.clone();
+ cx.spawn(async move |cx| {
+ delegate.set_status(Some("Waiting for existing connection attempt"), cx);
+ })
+ .detach();
+ return task.clone();
+ }
+ Some(ConnectionPoolEntry::Connected(ssh)) => {
+ if let Some(ssh) = ssh.upgrade()
+ && !ssh.has_been_killed()
+ {
+ return Task::ready(Ok(ssh)).shared();
+ }
+ self.connections.remove(&opts);
+ }
+ None => {}
+ }
+
+ let task = cx
+ .spawn({
+ let opts = opts.clone();
+ let delegate = delegate.clone();
+ async move |cx| {
+ let connection = SshRemoteConnection::new(opts.clone(), delegate, cx)
+ .await
+ .map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>);
+
+ cx.update_global(|pool: &mut Self, _| {
+ debug_assert!(matches!(
+ pool.connections.get(&opts),
+ Some(ConnectionPoolEntry::Connecting(_))
+ ));
+ match connection {
+ Ok(connection) => {
+ pool.connections.insert(
+ opts.clone(),
+ ConnectionPoolEntry::Connected(Arc::downgrade(&connection)),
+ );
+ Ok(connection)
+ }
+ Err(error) => {
+ pool.connections.remove(&opts);
+ Err(Arc::new(error))
+ }
+ }
+ })?
+ }
+ })
+ .shared();
+
+ self.connections
+ .insert(opts.clone(), ConnectionPoolEntry::Connecting(task.clone()));
+ task
+ }
+}
+
+#[async_trait(?Send)]
+pub(crate) trait RemoteConnection: Send + Sync {
+ fn start_proxy(
+ &self,
+ unique_identifier: String,
+ reconnect: bool,
+ incoming_tx: UnboundedSender<Envelope>,
+ outgoing_rx: UnboundedReceiver<Envelope>,
+ connection_activity_tx: Sender<()>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<i32>>;
+ fn upload_directory(
+ &self,
+ src_path: PathBuf,
+ dest_path: RemotePathBuf,
+ cx: &App,
+ ) -> Task<Result<()>>;
+ async fn kill(&self) -> Result<()>;
+ fn has_been_killed(&self) -> bool;
+ fn build_command(
+ &self,
+ program: Option<String>,
+ args: &[String],
+ env: &HashMap<String, String>,
+ working_dir: Option<String>,
+ port_forward: Option<(u16, String, u16)>,
+ ) -> Result<CommandTemplate>;
+ fn connection_options(&self) -> SshConnectionOptions;
+ fn path_style(&self) -> PathStyle;
+ fn shell(&self) -> String;
+
+ #[cfg(any(test, feature = "test-support"))]
+ fn simulate_disconnect(&self, _: &AsyncApp) {}
+}
+
+type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
+
+struct ChannelClient {
+ next_message_id: AtomicU32,
+ outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>,
+ buffer: Mutex<VecDeque<Envelope>>,
+ response_channels: ResponseChannels,
+ message_handlers: Mutex<ProtoMessageHandlerSet>,
+ max_received: AtomicU32,
+ name: &'static str,
+ task: Mutex<Task<Result<()>>>,
+}
+
+impl ChannelClient {
+ fn new(
+ incoming_rx: mpsc::UnboundedReceiver<Envelope>,
+ outgoing_tx: mpsc::UnboundedSender<Envelope>,
+ cx: &App,
+ name: &'static str,
+ ) -> Arc<Self> {
+ Arc::new_cyclic(|this| Self {
+ outgoing_tx: Mutex::new(outgoing_tx),
+ next_message_id: AtomicU32::new(0),
+ max_received: AtomicU32::new(0),
+ response_channels: ResponseChannels::default(),
+ message_handlers: Default::default(),
+ buffer: Mutex::new(VecDeque::new()),
+ name,
+ task: Mutex::new(Self::start_handling_messages(
+ this.clone(),
+ incoming_rx,
+ &cx.to_async(),
+ )),
+ })
+ }
+
+ fn start_handling_messages(
+ this: Weak<Self>,
+ mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
+ cx: &AsyncApp,
+ ) -> Task<Result<()>> {
+ cx.spawn(async move |cx| {
+ let peer_id = PeerId { owner_id: 0, id: 0 };
+ while let Some(incoming) = incoming_rx.next().await {
+ let Some(this) = this.upgrade() else {
+ return anyhow::Ok(());
+ };
+ if let Some(ack_id) = incoming.ack_id {
+ let mut buffer = this.buffer.lock();
+ while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
+ buffer.pop_front();
+ }
+ }
+ if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload
+ {
+ log::debug!(
+ "{}:ssh message received. name:FlushBufferedMessages",
+ this.name
+ );
+ {
+ let buffer = this.buffer.lock();
+ for envelope in buffer.iter() {
+ this.outgoing_tx
+ .lock()
+ .unbounded_send(envelope.clone())
+ .ok();
+ }
+ }
+ let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None);
+ envelope.id = this.next_message_id.fetch_add(1, SeqCst);
+ this.outgoing_tx.lock().unbounded_send(envelope).ok();
+ continue;
+ }
+
+ this.max_received.store(incoming.id, SeqCst);
+
+ if let Some(request_id) = incoming.responding_to {
+ let request_id = MessageId(request_id);
+ let sender = this.response_channels.lock().remove(&request_id);
+ if let Some(sender) = sender {
+ let (tx, rx) = oneshot::channel();
+ if incoming.payload.is_some() {
+ sender.send((incoming, tx)).ok();
+ }
+ rx.await.ok();
+ }
+ } else if let Some(envelope) =
+ build_typed_envelope(peer_id, Instant::now(), incoming)
+ {
+ let type_name = envelope.payload_type_name();
+ let message_id = envelope.message_id();
+ if let Some(future) = ProtoMessageHandlerSet::handle_message(
+ &this.message_handlers,
+ envelope,
+ this.clone().into(),
+ cx.clone(),
+ ) {
+ log::debug!("{}:ssh message received. name:{type_name}", this.name);
+ cx.foreground_executor()
+ .spawn(async move {
+ match future.await {
+ Ok(_) => {
+ log::debug!(
+ "{}:ssh message handled. name:{type_name}",
+ this.name
+ );
+ }
+ Err(error) => {
+ log::error!(
+ "{}:error handling message. type:{}, error:{}",
+ this.name,
+ type_name,
+ format!("{error:#}").lines().fold(
+ String::new(),
+ |mut message, line| {
+ if !message.is_empty() {
+ message.push(' ');
+ }
+ message.push_str(line);
+ message
+ }
+ )
+ );
+ }
+ }
+ })
+ .detach()
+ } else {
+ log::error!("{}:unhandled ssh message name:{type_name}", this.name);
+ if let Err(e) = AnyProtoClient::from(this.clone()).send_response(
+ message_id,
+ anyhow::anyhow!("no handler registered for {type_name}").to_proto(),
+ ) {
+ log::error!(
+ "{}:error sending error response for {type_name}:{e:#}",
+ this.name
+ );
+ }
+ }
+ }
+ }
+ anyhow::Ok(())
+ })
+ }
+
+ fn reconnect(
+ self: &Arc<Self>,
+ incoming_rx: UnboundedReceiver<Envelope>,
+ outgoing_tx: UnboundedSender<Envelope>,
+ cx: &AsyncApp,
+ ) {
+ *self.outgoing_tx.lock() = outgoing_tx;
+ *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx);
+ }
+
+ fn request<T: RequestMessage>(
+ &self,
+ payload: T,
+ ) -> impl 'static + Future<Output = Result<T::Response>> {
+ self.request_internal(payload, true)
+ }
+
+ fn request_internal<T: RequestMessage>(
+ &self,
+ payload: T,
+ use_buffer: bool,
+ ) -> impl 'static + Future<Output = Result<T::Response>> {
+ log::debug!("ssh request start. name:{}", T::NAME);
+ let response =
+ self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer);
+ async move {
+ let response = response.await?;
+ log::debug!("ssh request finish. name:{}", T::NAME);
+ T::Response::from_envelope(response).context("received a response of the wrong type")
+ }
+ }
+
+ async fn resync(&self, timeout: Duration) -> Result<()> {
+ smol::future::or(
+ async {
+ self.request_internal(proto::FlushBufferedMessages {}, false)
+ .await?;
+
+ for envelope in self.buffer.lock().iter() {
+ self.outgoing_tx
+ .lock()
+ .unbounded_send(envelope.clone())
+ .ok();
+ }
+ Ok(())
+ },
+ async {
+ smol::Timer::after(timeout).await;
+ anyhow::bail!("Timed out resyncing remote client")
+ },
+ )
+ .await
+ }
+
+ async fn ping(&self, timeout: Duration) -> Result<()> {
+ smol::future::or(
+ async {
+ self.request(proto::Ping {}).await?;
+ Ok(())
+ },
+ async {
+ smol::Timer::after(timeout).await;
+ anyhow::bail!("Timed out pinging remote client")
+ },
+ )
+ .await
+ }
+
+ fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
+ log::debug!("ssh send name:{}", T::NAME);
+ self.send_dynamic(payload.into_envelope(0, None, None))
+ }
+
+ fn request_dynamic(
+ &self,
+ mut envelope: proto::Envelope,
+ type_name: &'static str,
+ use_buffer: bool,
+ ) -> impl 'static + Future<Output = Result<proto::Envelope>> {
+ envelope.id = self.next_message_id.fetch_add(1, SeqCst);
+ let (tx, rx) = oneshot::channel();
+ let mut response_channels_lock = self.response_channels.lock();
+ response_channels_lock.insert(MessageId(envelope.id), tx);
+ drop(response_channels_lock);
+
+ let result = if use_buffer {
+ self.send_buffered(envelope)
+ } else {
+ self.send_unbuffered(envelope)
+ };
+ async move {
+ if let Err(error) = &result {
+ log::error!("failed to send message: {error}");
+ anyhow::bail!("failed to send message: {error}");
+ }
+
+ let response = rx.await.context("connection lost")?.0;
+ if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
+ return Err(RpcError::from_proto(error, type_name));
+ }
+ Ok(response)
+ }
+ }
+
+ pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
+ envelope.id = self.next_message_id.fetch_add(1, SeqCst);
+ self.send_buffered(envelope)
+ }
+
+ fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
+ envelope.ack_id = Some(self.max_received.load(SeqCst));
+ self.buffer.lock().push_back(envelope.clone());
+ // ignore errors on send (happen while we're reconnecting)
+ // assume that the global "disconnected" overlay is sufficient.
+ self.outgoing_tx.lock().unbounded_send(envelope).ok();
+ Ok(())
+ }
+
+ fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> {
+ envelope.ack_id = Some(self.max_received.load(SeqCst));
+ self.outgoing_tx.lock().unbounded_send(envelope).ok();
+ Ok(())
+ }
+}
+
+impl ProtoClient for ChannelClient {
+ fn request(
+ &self,
+ envelope: proto::Envelope,
+ request_type: &'static str,
+ ) -> BoxFuture<'static, Result<proto::Envelope>> {
+ self.request_dynamic(envelope, request_type, true).boxed()
+ }
+
+ fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
+ self.send_dynamic(envelope)
+ }
+
+ fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> {
+ self.send_dynamic(envelope)
+ }
+
+ fn message_handler_set(&self) -> &Mutex<ProtoMessageHandlerSet> {
+ &self.message_handlers
+ }
+
+ fn is_via_collab(&self) -> bool {
+ false
+ }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+mod fake {
+ use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform};
+ use crate::{SshConnectionOptions, remote_client::CommandTemplate};
+ use anyhow::Result;
+ use async_trait::async_trait;
+ use collections::HashMap;
+ use futures::{
+ FutureExt, SinkExt, StreamExt,
+ channel::{
+ mpsc::{self, Sender},
+ oneshot,
+ },
+ select_biased,
+ };
+ use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext};
+ use release_channel::ReleaseChannel;
+ use rpc::proto::Envelope;
+ use std::{path::PathBuf, sync::Arc};
+ use util::paths::{PathStyle, RemotePathBuf};
+
+ pub(super) struct FakeRemoteConnection {
+ pub(super) connection_options: SshConnectionOptions,
+ pub(super) server_channel: Arc<ChannelClient>,
+ pub(super) server_cx: SendableCx,
+ }
+
+ pub(super) struct SendableCx(AsyncApp);
+ impl SendableCx {
+ // SAFETY: When run in test mode, GPUI is always single threaded.
+ pub(super) fn new(cx: &TestAppContext) -> Self {
+ Self(cx.to_async())
+ }
+
+ // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp
+ fn get(&self, _: &AsyncApp) -> AsyncApp {
+ self.0.clone()
+ }
+ }
+
+ // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`]
+ unsafe impl Send for SendableCx {}
+ unsafe impl Sync for SendableCx {}
+
+ #[async_trait(?Send)]
+ impl RemoteConnection for FakeRemoteConnection {
+ async fn kill(&self) -> Result<()> {
+ Ok(())
+ }
+
+ fn has_been_killed(&self) -> bool {
+ false
+ }
+
+ fn build_command(
+ &self,
+ program: Option<String>,
+ args: &[String],
+ env: &HashMap<String, String>,
+ _: Option<String>,
+ _: Option<(u16, String, u16)>,
+ ) -> Result<CommandTemplate> {
+ let ssh_program = program.unwrap_or_else(|| "sh".to_string());
+ let mut ssh_args = Vec::new();
+ ssh_args.push(ssh_program);
+ ssh_args.extend(args.iter().cloned());
+ Ok(CommandTemplate {
+ program: "ssh".into(),
+ args: ssh_args,
+ env: env.clone(),
+ })
+ }
+
+ fn upload_directory(
+ &self,
+ _src_path: PathBuf,
+ _dest_path: RemotePathBuf,
+ _cx: &App,
+ ) -> Task<Result<()>> {
+ unreachable!()
+ }
+
+ fn connection_options(&self) -> SshConnectionOptions {
+ self.connection_options.clone()
+ }
+
+ fn simulate_disconnect(&self, cx: &AsyncApp) {
+ let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
+ let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
+ self.server_channel
+ .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
+ }
+
+ fn start_proxy(
+ &self,
+ _unique_identifier: String,
+ _reconnect: bool,
+ mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
+ mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
+ mut connection_activity_tx: Sender<()>,
+ _delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<i32>> {
+ let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
+ let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
+
+ self.server_channel.reconnect(
+ server_incoming_rx,
+ server_outgoing_tx,
+ &self.server_cx.get(cx),
+ );
+
+ cx.background_spawn(async move {
+ loop {
+ select_biased! {
+ server_to_client = server_outgoing_rx.next().fuse() => {
+ let Some(server_to_client) = server_to_client else {
+ return Ok(1)
+ };
+ connection_activity_tx.try_send(()).ok();
+ client_incoming_tx.send(server_to_client).await.ok();
+ }
+ client_to_server = client_outgoing_rx.next().fuse() => {
+ let Some(client_to_server) = client_to_server else {
+ return Ok(1)
+ };
+ server_incoming_tx.send(client_to_server).await.ok();
+ }
+ }
+ }
+ })
+ }
+
+ fn path_style(&self) -> PathStyle {
+ PathStyle::current()
+ }
+
+ fn shell(&self) -> String {
+ "sh".to_owned()
+ }
+ }
+
+ pub(super) struct Delegate;
+
+ impl RemoteClientDelegate for Delegate {
+ fn ask_password(&self, _: String, _: oneshot::Sender<String>, _: &mut AsyncApp) {
+ unreachable!()
+ }
+
+ fn download_server_binary_locally(
+ &self,
+ _: RemotePlatform,
+ _: ReleaseChannel,
+ _: Option<SemanticVersion>,
+ _: &mut AsyncApp,
+ ) -> Task<Result<PathBuf>> {
+ unreachable!()
+ }
+
+ fn get_download_params(
+ &self,
+ _platform: RemotePlatform,
+ _release_channel: ReleaseChannel,
+ _version: Option<SemanticVersion>,
+ _cx: &mut AsyncApp,
+ ) -> Task<Result<Option<(String, String)>>> {
+ unreachable!()
+ }
+
+ fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {}
+ }
+}
@@ -1,2749 +0,0 @@
-use crate::{
- json_log::LogRecord,
- protocol::{
- MESSAGE_LEN_SIZE, MessageId, message_len_from_buffer, read_message_with_len, write_message,
- },
- proxy::ProxyLaunchError,
-};
-use anyhow::{Context as _, Result, anyhow};
-use async_trait::async_trait;
-use collections::HashMap;
-use futures::{
- AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
- channel::{
- mpsc::{self, Sender, UnboundedReceiver, UnboundedSender},
- oneshot,
- },
- future::{BoxFuture, Shared},
- select, select_biased,
-};
-use gpui::{
- App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity,
- EventEmitter, Global, SemanticVersion, Task, WeakEntity,
-};
-use itertools::Itertools;
-use parking_lot::Mutex;
-
-use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
-use rpc::{
- AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError,
- proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope},
-};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use smol::{
- fs,
- process::{self, Child, Stdio},
-};
-use std::{
- collections::VecDeque,
- fmt, iter,
- ops::ControlFlow,
- path::{Path, PathBuf},
- sync::{
- Arc, Weak,
- atomic::{AtomicU32, AtomicU64, Ordering::SeqCst},
- },
- time::{Duration, Instant},
-};
-use tempfile::TempDir;
-use util::{
- ResultExt,
- paths::{PathStyle, RemotePathBuf},
-};
-
-#[derive(Clone)]
-pub struct SshSocket {
- connection_options: SshConnectionOptions,
- #[cfg(not(target_os = "windows"))]
- socket_path: PathBuf,
- #[cfg(target_os = "windows")]
- envs: HashMap<String, String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
-pub struct SshPortForwardOption {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub local_host: Option<String>,
- pub local_port: u16,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub remote_host: Option<String>,
- pub remote_port: u16,
-}
-
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
-pub struct SshConnectionOptions {
- pub host: String,
- pub username: Option<String>,
- pub port: Option<u16>,
- pub password: Option<String>,
- pub args: Option<Vec<String>>,
- pub port_forwards: Option<Vec<SshPortForwardOption>>,
-
- pub nickname: Option<String>,
- pub upload_binary_over_ssh: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SshArgs {
- pub arguments: Vec<String>,
- pub envs: Option<HashMap<String, String>>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SshInfo {
- pub args: SshArgs,
- pub path_style: PathStyle,
- pub shell: String,
-}
-
-#[macro_export]
-macro_rules! shell_script {
- ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
- format!(
- $fmt,
- $(
- $name = shlex::try_quote($arg).unwrap()
- ),+
- )
- }};
-}
-
-fn parse_port_number(port_str: &str) -> Result<u16> {
- port_str
- .parse()
- .with_context(|| format!("parsing port number: {port_str}"))
-}
-
-fn parse_port_forward_spec(spec: &str) -> Result<SshPortForwardOption> {
- let parts: Vec<&str> = spec.split(':').collect();
-
- match parts.len() {
- 4 => {
- let local_port = parse_port_number(parts[1])?;
- let remote_port = parse_port_number(parts[3])?;
-
- Ok(SshPortForwardOption {
- local_host: Some(parts[0].to_string()),
- local_port,
- remote_host: Some(parts[2].to_string()),
- remote_port,
- })
- }
- 3 => {
- let local_port = parse_port_number(parts[0])?;
- let remote_port = parse_port_number(parts[2])?;
-
- Ok(SshPortForwardOption {
- local_host: None,
- local_port,
- remote_host: Some(parts[1].to_string()),
- remote_port,
- })
- }
- _ => anyhow::bail!("Invalid port forward format"),
- }
-}
-
-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();
- let mut port_forwards: Vec<SshPortForwardOption> = 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", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R",
- "-w",
- ];
-
- let mut tokens = shlex::split(input).context("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;
- }
- if arg == "-L" || arg.starts_with("-L") {
- let forward_spec = if arg == "-L" {
- tokens.next()
- } else {
- Some(arg.strip_prefix("-L").unwrap().to_string())
- };
-
- if let Some(spec) = forward_spec {
- port_forwards.push(parse_port_forward_spec(&spec)?);
- } else {
- anyhow::bail!("Missing port forward format");
- }
- }
-
- 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;
- // Destination might be: username1@username2@ip2@ip1
- if let Some((u, rest)) = input.rsplit_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");
- };
-
- let port_forwards = match port_forwards.len() {
- 0 => None,
- _ => Some(port_forwards),
- };
-
- Ok(Self {
- host: hostname,
- username,
- port,
- port_forwards,
- args: Some(args),
- password: None,
- nickname: None,
- upload_binary_over_ssh: false,
- })
- }
-
- pub fn ssh_url(&self) -> String {
- let mut result = String::from("ssh://");
- if let Some(username) = &self.username {
- // Username might be: username1@username2@ip2
- let username = urlencoding::encode(username);
- result.push_str(&username);
- result.push('@');
- }
- result.push_str(&self.host);
- if let Some(port) = self.port {
- result.push(':');
- result.push_str(&port.to_string());
- }
- result
- }
-
- pub fn additional_args(&self) -> Vec<String> {
- let mut args = self.args.iter().flatten().cloned().collect::<Vec<String>>();
-
- if let Some(forwards) = &self.port_forwards {
- args.extend(forwards.iter().map(|pf| {
- let local_host = match &pf.local_host {
- Some(host) => host,
- None => "localhost",
- };
- let remote_host = match &pf.remote_host {
- Some(host) => host,
- None => "localhost",
- };
-
- format!(
- "-L{}:{}:{}:{}",
- local_host, pf.local_port, remote_host, pf.remote_port
- )
- }));
- }
-
- args
- }
-
- fn scp_url(&self) -> String {
- if let Some(username) = &self.username {
- format!("{}@{}", username, self.host)
- } else {
- self.host.clone()
- }
- }
-
- pub fn connection_string(&self) -> String {
- let host = if let Some(username) = &self.username {
- format!("{}@{}", username, self.host)
- } else {
- self.host.clone()
- };
- if let Some(port) = &self.port {
- format!("{}:{}", host, port)
- } else {
- host
- }
- }
-}
-
-#[derive(Copy, Clone, Debug)]
-pub struct SshPlatform {
- pub os: &'static str,
- pub arch: &'static str,
-}
-
-pub trait SshClientDelegate: Send + Sync {
- fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp);
- fn get_download_params(
- &self,
- platform: SshPlatform,
- release_channel: ReleaseChannel,
- version: Option<SemanticVersion>,
- cx: &mut AsyncApp,
- ) -> Task<Result<Option<(String, String)>>>;
-
- fn download_server_binary_locally(
- &self,
- platform: SshPlatform,
- release_channel: ReleaseChannel,
- version: Option<SemanticVersion>,
- cx: &mut AsyncApp,
- ) -> Task<Result<PathBuf>>;
- fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp);
-}
-
-impl SshSocket {
- #[cfg(not(target_os = "windows"))]
- fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result<Self> {
- Ok(Self {
- connection_options: options,
- socket_path,
- })
- }
-
- #[cfg(target_os = "windows")]
- fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result<Self> {
- let askpass_script = temp_dir.path().join("askpass.bat");
- std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?;
- let mut envs = HashMap::default();
- envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into());
- envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string());
- envs.insert("ZED_SSH_ASKPASS".into(), secret);
- Ok(Self {
- connection_options: options,
- envs,
- })
- }
-
- // :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
- // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
- // and passes -l as an argument to sh, not to ls.
- // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing
- // into a machine. You must use `cd` to get back to $HOME.
- // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'"
- fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command {
- let mut command = util::command::new_smol_command("ssh");
- let to_run = iter::once(&program)
- .chain(args.iter())
- .map(|token| {
- // We're trying to work with: sh, bash, zsh, fish, tcsh, ...?
- debug_assert!(
- !token.contains('\n'),
- "multiline arguments do not work in all shells"
- );
- shlex::try_quote(token).unwrap()
- })
- .join(" ");
- let to_run = format!("cd; {to_run}");
- log::debug!("ssh {} {:?}", self.connection_options.ssh_url(), to_run);
- self.ssh_options(&mut command)
- .arg(self.connection_options.ssh_url())
- .arg(to_run);
- command
- }
-
- async fn run_command(&self, program: &str, args: &[&str]) -> Result<String> {
- let output = self.ssh_command(program, args).output().await?;
- anyhow::ensure!(
- output.status.success(),
- "failed to run command: {}",
- String::from_utf8_lossy(&output.stderr)
- );
- Ok(String::from_utf8_lossy(&output.stdout).to_string())
- }
-
- #[cfg(not(target_os = "windows"))]
- fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
- command
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .args(self.connection_options.additional_args())
- .args(["-o", "ControlMaster=no", "-o"])
- .arg(format!("ControlPath={}", self.socket_path.display()))
- }
-
- #[cfg(target_os = "windows")]
- fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
- command
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .args(self.connection_options.additional_args())
- .envs(self.envs.clone())
- }
-
- // 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
- #[cfg(not(target_os = "windows"))]
- fn ssh_args(&self) -> SshArgs {
- let mut arguments = self.connection_options.additional_args();
- arguments.extend(vec![
- "-o".to_string(),
- "ControlMaster=no".to_string(),
- "-o".to_string(),
- format!("ControlPath={}", self.socket_path.display()),
- self.connection_options.ssh_url(),
- ]);
- SshArgs {
- arguments,
- envs: None,
- }
- }
-
- #[cfg(target_os = "windows")]
- fn ssh_args(&self) -> SshArgs {
- let mut arguments = self.connection_options.additional_args();
- arguments.push(self.connection_options.ssh_url());
- SshArgs {
- arguments,
- envs: Some(self.envs.clone()),
- }
- }
-
- async fn platform(&self) -> Result<SshPlatform> {
- let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?;
- let Some((os, arch)) = uname.split_once(" ") else {
- anyhow::bail!("unknown uname: {uname:?}")
- };
-
- let os = match os.trim() {
- "Darwin" => "macos",
- "Linux" => "linux",
- _ => anyhow::bail!(
- "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
- ),
- };
- // exclude armv5,6,7 as they are 32-bit.
- let arch = if arch.starts_with("armv8")
- || arch.starts_with("armv9")
- || arch.starts_with("arm64")
- || arch.starts_with("aarch64")
- {
- "aarch64"
- } else if arch.starts_with("x86") {
- "x86_64"
- } else {
- anyhow::bail!(
- "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
- )
- };
-
- Ok(SshPlatform { os, arch })
- }
-
- async fn shell(&self) -> String {
- match self.run_command("sh", &["-lc", "echo $SHELL"]).await {
- Ok(shell) => shell.trim().to_owned(),
- Err(e) => {
- log::error!("Failed to get shell: {e}");
- "sh".to_owned()
- }
- }
- }
-}
-
-const MAX_MISSED_HEARTBEATS: usize = 5;
-const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
-const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5);
-
-const MAX_RECONNECT_ATTEMPTS: usize = 3;
-
-enum State {
- Connecting,
- Connected {
- ssh_connection: Arc<dyn RemoteConnection>,
- delegate: Arc<dyn SshClientDelegate>,
-
- multiplex_task: Task<Result<()>>,
- heartbeat_task: Task<Result<()>>,
- },
- HeartbeatMissed {
- missed_heartbeats: usize,
-
- ssh_connection: Arc<dyn RemoteConnection>,
- delegate: Arc<dyn SshClientDelegate>,
-
- multiplex_task: Task<Result<()>>,
- heartbeat_task: Task<Result<()>>,
- },
- Reconnecting,
- ReconnectFailed {
- ssh_connection: Arc<dyn RemoteConnection>,
- delegate: Arc<dyn SshClientDelegate>,
-
- error: anyhow::Error,
- attempts: usize,
- },
- ReconnectExhausted,
- ServerNotRunning,
-}
-
-impl fmt::Display for State {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Connecting => write!(f, "connecting"),
- Self::Connected { .. } => write!(f, "connected"),
- Self::Reconnecting => write!(f, "reconnecting"),
- Self::ReconnectFailed { .. } => write!(f, "reconnect failed"),
- Self::ReconnectExhausted => write!(f, "reconnect exhausted"),
- Self::HeartbeatMissed { .. } => write!(f, "heartbeat missed"),
- Self::ServerNotRunning { .. } => write!(f, "server not running"),
- }
- }
-}
-
-impl State {
- fn ssh_connection(&self) -> Option<&dyn RemoteConnection> {
- match self {
- Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()),
- Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
- Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
- _ => None,
- }
- }
-
- fn can_reconnect(&self) -> bool {
- match self {
- Self::Connected { .. }
- | Self::HeartbeatMissed { .. }
- | Self::ReconnectFailed { .. } => true,
- State::Connecting
- | State::Reconnecting
- | State::ReconnectExhausted
- | State::ServerNotRunning => false,
- }
- }
-
- fn is_reconnect_failed(&self) -> bool {
- matches!(self, Self::ReconnectFailed { .. })
- }
-
- fn is_reconnect_exhausted(&self) -> bool {
- matches!(self, Self::ReconnectExhausted { .. })
- }
-
- fn is_server_not_running(&self) -> bool {
- matches!(self, Self::ServerNotRunning)
- }
-
- fn is_reconnecting(&self) -> bool {
- matches!(self, Self::Reconnecting { .. })
- }
-
- fn heartbeat_recovered(self) -> Self {
- match self {
- Self::HeartbeatMissed {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- ..
- } => Self::Connected {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- },
- _ => self,
- }
- }
-
- fn heartbeat_missed(self) -> Self {
- match self {
- Self::Connected {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- } => Self::HeartbeatMissed {
- missed_heartbeats: 1,
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- },
- Self::HeartbeatMissed {
- missed_heartbeats,
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- } => Self::HeartbeatMissed {
- missed_heartbeats: missed_heartbeats + 1,
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- },
- _ => self,
- }
- }
-}
-
-/// The state of the ssh connection.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum ConnectionState {
- Connecting,
- Connected,
- HeartbeatMissed,
- Reconnecting,
- Disconnected,
-}
-
-impl From<&State> for ConnectionState {
- fn from(value: &State) -> Self {
- match value {
- State::Connecting => Self::Connecting,
- State::Connected { .. } => Self::Connected,
- State::Reconnecting | State::ReconnectFailed { .. } => Self::Reconnecting,
- State::HeartbeatMissed { .. } => Self::HeartbeatMissed,
- State::ReconnectExhausted => Self::Disconnected,
- State::ServerNotRunning => Self::Disconnected,
- }
- }
-}
-
-pub struct SshRemoteClient {
- client: Arc<ChannelClient>,
- unique_identifier: String,
- connection_options: SshConnectionOptions,
- path_style: PathStyle,
- state: Arc<Mutex<Option<State>>>,
-}
-
-#[derive(Debug)]
-pub enum SshRemoteEvent {
- Disconnected,
-}
-
-impl EventEmitter<SshRemoteEvent> for SshRemoteClient {}
-
-// Identifies the socket on the remote server so that reconnects
-// can re-join the same project.
-pub enum ConnectionIdentifier {
- Setup(u64),
- Workspace(i64),
-}
-
-static NEXT_ID: AtomicU64 = AtomicU64::new(1);
-
-impl ConnectionIdentifier {
- pub fn setup() -> Self {
- Self::Setup(NEXT_ID.fetch_add(1, SeqCst))
- }
-
- // This string gets used in a socket name, and so must be relatively short.
- // The total length of:
- // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock
- // Must be less than about 100 characters
- // https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars
- // So our strings should be at most 20 characters or so.
- fn to_string(&self, cx: &App) -> String {
- let identifier_prefix = match ReleaseChannel::global(cx) {
- ReleaseChannel::Stable => "".to_string(),
- release_channel => format!("{}-", release_channel.dev_name()),
- };
- match self {
- Self::Setup(setup_id) => format!("{identifier_prefix}setup-{setup_id}"),
- Self::Workspace(workspace_id) => {
- format!("{identifier_prefix}workspace-{workspace_id}",)
- }
- }
- }
-}
-
-impl SshRemoteClient {
- pub fn new(
- unique_identifier: ConnectionIdentifier,
- connection_options: SshConnectionOptions,
- cancellation: oneshot::Receiver<()>,
- delegate: Arc<dyn SshClientDelegate>,
- cx: &mut App,
- ) -> Task<Result<Option<Entity<Self>>>> {
- let unique_identifier = unique_identifier.to_string(cx);
- cx.spawn(async move |cx| {
- let success = Box::pin(async move {
- let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
- let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
- let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
-
- let client =
- cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
-
- let ssh_connection = cx
- .update(|cx| {
- cx.update_default_global(|pool: &mut ConnectionPool, cx| {
- pool.connect(connection_options.clone(), &delegate, cx)
- })
- })?
- .await
- .map_err(|e| e.cloned())?;
-
- let path_style = ssh_connection.path_style();
- let this = cx.new(|_| Self {
- client: client.clone(),
- unique_identifier: unique_identifier.clone(),
- connection_options,
- path_style,
- state: Arc::new(Mutex::new(Some(State::Connecting))),
- })?;
-
- let io_task = ssh_connection.start_proxy(
- unique_identifier,
- false,
- incoming_tx,
- outgoing_rx,
- connection_activity_tx,
- delegate.clone(),
- cx,
- );
-
- let multiplex_task = Self::monitor(this.downgrade(), io_task, cx);
-
- if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
- log::error!("failed to establish connection: {}", error);
- return Err(error);
- }
-
- let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx);
-
- this.update(cx, |this, _| {
- *this.state.lock() = Some(State::Connected {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- });
- })?;
-
- Ok(Some(this))
- });
-
- select! {
- _ = cancellation.fuse() => {
- Ok(None)
- }
- result = success.fuse() => result
- }
- })
- }
-
- pub fn proto_client_from_channels(
- incoming_rx: mpsc::UnboundedReceiver<Envelope>,
- outgoing_tx: mpsc::UnboundedSender<Envelope>,
- cx: &App,
- name: &'static str,
- ) -> AnyProtoClient {
- ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into()
- }
-
- pub fn shutdown_processes<T: RequestMessage>(
- &self,
- shutdown_request: Option<T>,
- executor: BackgroundExecutor,
- ) -> Option<impl Future<Output = ()> + use<T>> {
- let state = self.state.lock().take()?;
- log::info!("shutting down ssh processes");
-
- let State::Connected {
- multiplex_task,
- heartbeat_task,
- ssh_connection,
- delegate,
- } = state
- else {
- return None;
- };
-
- let client = self.client.clone();
-
- Some(async move {
- if let Some(shutdown_request) = shutdown_request {
- client.send(shutdown_request).log_err();
- // We wait 50ms instead of waiting for a response, because
- // waiting for a response would require us to wait on the main thread
- // which we want to avoid in an `on_app_quit` callback.
- executor.timer(Duration::from_millis(50)).await;
- }
-
- // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a
- // child of master_process.
- drop(multiplex_task);
- // Now drop the rest of state, which kills master process.
- drop(heartbeat_task);
- drop(ssh_connection);
- drop(delegate);
- })
- }
-
- fn reconnect(&mut self, cx: &mut Context<Self>) -> Result<()> {
- let mut lock = self.state.lock();
-
- let can_reconnect = lock
- .as_ref()
- .map(|state| state.can_reconnect())
- .unwrap_or(false);
- if !can_reconnect {
- log::info!("aborting reconnect, because not in state that allows reconnecting");
- let error = if let Some(state) = lock.as_ref() {
- format!("invalid state, cannot reconnect while in state {state}")
- } else {
- "no state set".to_string()
- };
- anyhow::bail!(error);
- }
-
- let state = lock.take().unwrap();
- let (attempts, ssh_connection, delegate) = match state {
- State::Connected {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- }
- | State::HeartbeatMissed {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task,
- ..
- } => {
- drop(multiplex_task);
- drop(heartbeat_task);
- (0, ssh_connection, delegate)
- }
- State::ReconnectFailed {
- attempts,
- ssh_connection,
- delegate,
- ..
- } => (attempts, ssh_connection, delegate),
- State::Connecting
- | State::Reconnecting
- | State::ReconnectExhausted
- | State::ServerNotRunning => unreachable!(),
- };
-
- let attempts = attempts + 1;
- if attempts > MAX_RECONNECT_ATTEMPTS {
- log::error!(
- "Failed to reconnect to after {} attempts, giving up",
- MAX_RECONNECT_ATTEMPTS
- );
- drop(lock);
- self.set_state(State::ReconnectExhausted, cx);
- return Ok(());
- }
- drop(lock);
-
- self.set_state(State::Reconnecting, cx);
-
- log::info!("Trying to reconnect to ssh server... Attempt {}", attempts);
-
- let unique_identifier = self.unique_identifier.clone();
- let client = self.client.clone();
- let reconnect_task = cx.spawn(async move |this, cx| {
- macro_rules! failed {
- ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => {
- return State::ReconnectFailed {
- error: anyhow!($error),
- attempts: $attempts,
- ssh_connection: $ssh_connection,
- delegate: $delegate,
- };
- };
- }
-
- if let Err(error) = ssh_connection
- .kill()
- .await
- .context("Failed to kill ssh process")
- {
- failed!(error, attempts, ssh_connection, delegate);
- };
-
- let connection_options = ssh_connection.connection_options();
-
- let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
- let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
- let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
-
- let (ssh_connection, io_task) = match async {
- let ssh_connection = cx
- .update_global(|pool: &mut ConnectionPool, cx| {
- pool.connect(connection_options, &delegate, cx)
- })?
- .await
- .map_err(|error| error.cloned())?;
-
- let io_task = ssh_connection.start_proxy(
- unique_identifier,
- true,
- incoming_tx,
- outgoing_rx,
- connection_activity_tx,
- delegate.clone(),
- cx,
- );
- anyhow::Ok((ssh_connection, io_task))
- }
- .await
- {
- Ok((ssh_connection, io_task)) => (ssh_connection, io_task),
- Err(error) => {
- failed!(error, attempts, ssh_connection, delegate);
- }
- };
-
- let multiplex_task = Self::monitor(this.clone(), io_task, cx);
- client.reconnect(incoming_rx, outgoing_tx, cx);
-
- if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
- failed!(error, attempts, ssh_connection, delegate);
- };
-
- State::Connected {
- ssh_connection,
- delegate,
- multiplex_task,
- heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx),
- }
- });
-
- cx.spawn(async move |this, cx| {
- let new_state = reconnect_task.await;
- this.update(cx, |this, cx| {
- this.try_set_state(cx, |old_state| {
- if old_state.is_reconnecting() {
- match &new_state {
- State::Connecting
- | State::Reconnecting
- | State::HeartbeatMissed { .. }
- | State::ServerNotRunning => {}
- State::Connected { .. } => {
- log::info!("Successfully reconnected");
- }
- State::ReconnectFailed {
- error, attempts, ..
- } => {
- log::error!(
- "Reconnect attempt {} failed: {:?}. Starting new attempt...",
- attempts,
- error
- );
- }
- State::ReconnectExhausted => {
- log::error!("Reconnect attempt failed and all attempts exhausted");
- }
- }
- Some(new_state)
- } else {
- None
- }
- });
-
- if this.state_is(State::is_reconnect_failed) {
- this.reconnect(cx)
- } else if this.state_is(State::is_reconnect_exhausted) {
- Ok(())
- } else {
- log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
- Ok(())
- }
- })
- })
- .detach_and_log_err(cx);
-
- Ok(())
- }
-
- fn heartbeat(
- this: WeakEntity<Self>,
- mut connection_activity_rx: mpsc::Receiver<()>,
- cx: &mut AsyncApp,
- ) -> Task<Result<()>> {
- let Ok(client) = this.read_with(cx, |this, _| this.client.clone()) else {
- return Task::ready(Err(anyhow!("SshRemoteClient lost")));
- };
-
- cx.spawn(async move |cx| {
- let mut missed_heartbeats = 0;
-
- let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse();
- futures::pin_mut!(keepalive_timer);
-
- loop {
- select_biased! {
- result = connection_activity_rx.next().fuse() => {
- if result.is_none() {
- log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping.");
- return Ok(());
- }
-
- if missed_heartbeats != 0 {
- missed_heartbeats = 0;
- let _ =this.update(cx, |this, cx| {
- this.handle_heartbeat_result(missed_heartbeats, cx)
- })?;
- }
- }
- _ = keepalive_timer => {
- log::debug!("Sending heartbeat to server...");
-
- let result = select_biased! {
- _ = connection_activity_rx.next().fuse() => {
- Ok(())
- }
- ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => {
- ping_result
- }
- };
-
- if result.is_err() {
- missed_heartbeats += 1;
- log::warn!(
- "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.",
- HEARTBEAT_TIMEOUT,
- missed_heartbeats,
- MAX_MISSED_HEARTBEATS
- );
- } else if missed_heartbeats != 0 {
- missed_heartbeats = 0;
- } else {
- continue;
- }
-
- let result = this.update(cx, |this, cx| {
- this.handle_heartbeat_result(missed_heartbeats, cx)
- })?;
- if result.is_break() {
- return Ok(());
- }
- }
- }
-
- keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
- }
- })
- }
-
- fn handle_heartbeat_result(
- &mut self,
- missed_heartbeats: usize,
- cx: &mut Context<Self>,
- ) -> ControlFlow<()> {
- let state = self.state.lock().take().unwrap();
- let next_state = if missed_heartbeats > 0 {
- state.heartbeat_missed()
- } else {
- state.heartbeat_recovered()
- };
-
- self.set_state(next_state, cx);
-
- if missed_heartbeats >= MAX_MISSED_HEARTBEATS {
- log::error!(
- "Missed last {} heartbeats. Reconnecting...",
- missed_heartbeats
- );
-
- self.reconnect(cx)
- .context("failed to start reconnect process after missing heartbeats")
- .log_err();
- ControlFlow::Break(())
- } else {
- ControlFlow::Continue(())
- }
- }
-
- fn monitor(
- this: WeakEntity<Self>,
- io_task: Task<Result<i32>>,
- cx: &AsyncApp,
- ) -> Task<Result<()>> {
- cx.spawn(async move |cx| {
- let result = io_task.await;
-
- match result {
- Ok(exit_code) => {
- if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) {
- match error {
- ProxyLaunchError::ServerNotRunning => {
- log::error!("failed to reconnect because server is not running");
- this.update(cx, |this, cx| {
- this.set_state(State::ServerNotRunning, cx);
- })?;
- }
- }
- } else if exit_code > 0 {
- log::error!("proxy process terminated unexpectedly");
- this.update(cx, |this, cx| {
- this.reconnect(cx).ok();
- })?;
- }
- }
- Err(error) => {
- log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
- this.update(cx, |this, cx| {
- this.reconnect(cx).ok();
- })?;
- }
- }
-
- Ok(())
- })
- }
-
- fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool {
- self.state.lock().as_ref().is_some_and(check)
- }
-
- fn try_set_state(&self, cx: &mut Context<Self>, map: impl FnOnce(&State) -> Option<State>) {
- let mut lock = self.state.lock();
- let new_state = lock.as_ref().and_then(map);
-
- if let Some(new_state) = new_state {
- lock.replace(new_state);
- cx.notify();
- }
- }
-
- fn set_state(&self, state: State, cx: &mut Context<Self>) {
- log::info!("setting state to '{}'", &state);
-
- let is_reconnect_exhausted = state.is_reconnect_exhausted();
- let is_server_not_running = state.is_server_not_running();
- self.state.lock().replace(state);
-
- if is_reconnect_exhausted || is_server_not_running {
- cx.emit(SshRemoteEvent::Disconnected);
- }
- cx.notify();
- }
-
- pub fn ssh_info(&self) -> Option<SshInfo> {
- self.state
- .lock()
- .as_ref()
- .and_then(|state| state.ssh_connection())
- .map(|ssh_connection| SshInfo {
- args: ssh_connection.ssh_args(),
- path_style: ssh_connection.path_style(),
- shell: ssh_connection.shell(),
- })
- }
-
- pub fn upload_directory(
- &self,
- src_path: PathBuf,
- dest_path: RemotePathBuf,
- cx: &App,
- ) -> Task<Result<()>> {
- let state = self.state.lock();
- let Some(connection) = state.as_ref().and_then(|state| state.ssh_connection()) else {
- return Task::ready(Err(anyhow!("no ssh connection")));
- };
- connection.upload_directory(src_path, dest_path, cx)
- }
-
- pub fn proto_client(&self) -> AnyProtoClient {
- self.client.clone().into()
- }
-
- pub fn connection_string(&self) -> String {
- self.connection_options.connection_string()
- }
-
- pub fn connection_options(&self) -> SshConnectionOptions {
- self.connection_options.clone()
- }
-
- pub fn connection_state(&self) -> ConnectionState {
- self.state
- .lock()
- .as_ref()
- .map(ConnectionState::from)
- .unwrap_or(ConnectionState::Disconnected)
- }
-
- pub fn is_disconnected(&self) -> bool {
- self.connection_state() == ConnectionState::Disconnected
- }
-
- pub fn path_style(&self) -> PathStyle {
- self.path_style
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> {
- let opts = self.connection_options();
- client_cx.spawn(async move |cx| {
- let connection = cx
- .update_global(|c: &mut ConnectionPool, _| {
- if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) {
- c.clone()
- } else {
- panic!("missing test connection")
- }
- })
- .unwrap()
- .await
- .unwrap();
-
- connection.simulate_disconnect(cx);
- })
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn fake_server(
- client_cx: &mut gpui::TestAppContext,
- server_cx: &mut gpui::TestAppContext,
- ) -> (SshConnectionOptions, AnyProtoClient) {
- let port = client_cx
- .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
- let opts = SshConnectionOptions {
- host: "<fake>".to_string(),
- port: Some(port),
- ..Default::default()
- };
- let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
- let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
- let server_client =
- server_cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server"));
- let connection: Arc<dyn RemoteConnection> = Arc::new(fake::FakeRemoteConnection {
- connection_options: opts.clone(),
- server_cx: fake::SendableCx::new(server_cx),
- server_channel: server_client.clone(),
- });
-
- client_cx.update(|cx| {
- cx.update_default_global(|c: &mut ConnectionPool, cx| {
- c.connections.insert(
- opts.clone(),
- ConnectionPoolEntry::Connecting(
- cx.background_spawn({
- let connection = connection.clone();
- async move { Ok(connection.clone()) }
- })
- .shared(),
- ),
- );
- })
- });
-
- (opts, server_client.into())
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub async fn fake_client(
- opts: SshConnectionOptions,
- client_cx: &mut gpui::TestAppContext,
- ) -> Entity<Self> {
- let (_tx, rx) = oneshot::channel();
- client_cx
- .update(|cx| {
- Self::new(
- ConnectionIdentifier::setup(),
- opts,
- rx,
- Arc::new(fake::Delegate),
- cx,
- )
- })
- .await
- .unwrap()
- .unwrap()
- }
-}
-
-enum ConnectionPoolEntry {
- Connecting(Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>>),
- Connected(Weak<dyn RemoteConnection>),
-}
-
-#[derive(Default)]
-struct ConnectionPool {
- connections: HashMap<SshConnectionOptions, ConnectionPoolEntry>,
-}
-
-impl Global for ConnectionPool {}
-
-impl ConnectionPool {
- pub fn connect(
- &mut self,
- opts: SshConnectionOptions,
- delegate: &Arc<dyn SshClientDelegate>,
- cx: &mut App,
- ) -> Shared<Task<Result<Arc<dyn RemoteConnection>, Arc<anyhow::Error>>>> {
- let connection = self.connections.get(&opts);
- match connection {
- Some(ConnectionPoolEntry::Connecting(task)) => {
- let delegate = delegate.clone();
- cx.spawn(async move |cx| {
- delegate.set_status(Some("Waiting for existing connection attempt"), cx);
- })
- .detach();
- return task.clone();
- }
- Some(ConnectionPoolEntry::Connected(ssh)) => {
- if let Some(ssh) = ssh.upgrade()
- && !ssh.has_been_killed()
- {
- return Task::ready(Ok(ssh)).shared();
- }
- self.connections.remove(&opts);
- }
- None => {}
- }
-
- let task = cx
- .spawn({
- let opts = opts.clone();
- let delegate = delegate.clone();
- async move |cx| {
- let connection = SshRemoteConnection::new(opts.clone(), delegate, cx)
- .await
- .map(|connection| Arc::new(connection) as Arc<dyn RemoteConnection>);
-
- cx.update_global(|pool: &mut Self, _| {
- debug_assert!(matches!(
- pool.connections.get(&opts),
- Some(ConnectionPoolEntry::Connecting(_))
- ));
- match connection {
- Ok(connection) => {
- pool.connections.insert(
- opts.clone(),
- ConnectionPoolEntry::Connected(Arc::downgrade(&connection)),
- );
- Ok(connection)
- }
- Err(error) => {
- pool.connections.remove(&opts);
- Err(Arc::new(error))
- }
- }
- })?
- }
- })
- .shared();
-
- self.connections
- .insert(opts.clone(), ConnectionPoolEntry::Connecting(task.clone()));
- task
- }
-}
-
-impl From<SshRemoteClient> for AnyProtoClient {
- fn from(client: SshRemoteClient) -> Self {
- AnyProtoClient::new(client.client)
- }
-}
-
-#[async_trait(?Send)]
-trait RemoteConnection: Send + Sync {
- fn start_proxy(
- &self,
- unique_identifier: String,
- reconnect: bool,
- incoming_tx: UnboundedSender<Envelope>,
- outgoing_rx: UnboundedReceiver<Envelope>,
- connection_activity_tx: Sender<()>,
- delegate: Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Task<Result<i32>>;
- fn upload_directory(
- &self,
- src_path: PathBuf,
- dest_path: RemotePathBuf,
- cx: &App,
- ) -> Task<Result<()>>;
- async fn kill(&self) -> Result<()>;
- fn has_been_killed(&self) -> bool;
- /// 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) -> SshArgs;
- fn connection_options(&self) -> SshConnectionOptions;
- fn path_style(&self) -> PathStyle;
- fn shell(&self) -> String;
-
- #[cfg(any(test, feature = "test-support"))]
- fn simulate_disconnect(&self, _: &AsyncApp) {}
-}
-
-struct SshRemoteConnection {
- socket: SshSocket,
- master_process: Mutex<Option<Child>>,
- remote_binary_path: Option<RemotePathBuf>,
- ssh_platform: SshPlatform,
- ssh_path_style: PathStyle,
- ssh_shell: String,
- _temp_dir: TempDir,
-}
-
-#[async_trait(?Send)]
-impl RemoteConnection for SshRemoteConnection {
- async fn kill(&self) -> Result<()> {
- let Some(mut process) = self.master_process.lock().take() else {
- return Ok(());
- };
- process.kill().ok();
- process.status().await?;
- Ok(())
- }
-
- fn has_been_killed(&self) -> bool {
- self.master_process.lock().is_none()
- }
-
- fn ssh_args(&self) -> SshArgs {
- self.socket.ssh_args()
- }
-
- fn connection_options(&self) -> SshConnectionOptions {
- self.socket.connection_options.clone()
- }
-
- fn shell(&self) -> String {
- self.ssh_shell.clone()
- }
-
- fn upload_directory(
- &self,
- src_path: PathBuf,
- dest_path: RemotePathBuf,
- cx: &App,
- ) -> Task<Result<()>> {
- let mut command = util::command::new_smol_command("scp");
- let output = self
- .socket
- .ssh_options(&mut command)
- .args(
- self.socket
- .connection_options
- .port
- .map(|port| vec!["-P".to_string(), port.to_string()])
- .unwrap_or_default(),
- )
- .arg("-C")
- .arg("-r")
- .arg(&src_path)
- .arg(format!(
- "{}:{}",
- self.socket.connection_options.scp_url(),
- dest_path
- ))
- .output();
-
- cx.background_spawn(async move {
- let output = output.await?;
-
- anyhow::ensure!(
- output.status.success(),
- "failed to upload directory {} -> {}: {}",
- src_path.display(),
- dest_path.to_string(),
- String::from_utf8_lossy(&output.stderr)
- );
-
- Ok(())
- })
- }
-
- fn start_proxy(
- &self,
- unique_identifier: String,
- reconnect: bool,
- incoming_tx: UnboundedSender<Envelope>,
- outgoing_rx: UnboundedReceiver<Envelope>,
- connection_activity_tx: Sender<()>,
- delegate: Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Task<Result<i32>> {
- delegate.set_status(Some("Starting proxy"), cx);
-
- let Some(remote_binary_path) = self.remote_binary_path.clone() else {
- return Task::ready(Err(anyhow!("Remote binary path not set")));
- };
-
- let mut start_proxy_command = shell_script!(
- "exec {binary_path} proxy --identifier {identifier}",
- binary_path = &remote_binary_path.to_string(),
- identifier = &unique_identifier,
- );
-
- for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
- if let Some(value) = std::env::var(env_var).ok() {
- start_proxy_command = format!(
- "{}={} {} ",
- env_var,
- shlex::try_quote(&value).unwrap(),
- start_proxy_command,
- );
- }
- }
-
- if reconnect {
- start_proxy_command.push_str(" --reconnect");
- }
-
- let ssh_proxy_process = match self
- .socket
- .ssh_command("sh", &["-lc", &start_proxy_command])
- // IMPORTANT: we kill this process when we drop the task that uses it.
- .kill_on_drop(true)
- .spawn()
- {
- Ok(process) => process,
- Err(error) => {
- return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error)));
- }
- };
-
- Self::multiplex(
- ssh_proxy_process,
- incoming_tx,
- outgoing_rx,
- connection_activity_tx,
- cx,
- )
- }
-
- fn path_style(&self) -> PathStyle {
- self.ssh_path_style
- }
-}
-
-impl SshRemoteConnection {
- async fn new(
- connection_options: SshConnectionOptions,
- delegate: Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Result<Self> {
- use askpass::AskPassResult;
-
- delegate.set_status(Some("Connecting"), cx);
-
- let url = connection_options.ssh_url();
-
- let temp_dir = tempfile::Builder::new()
- .prefix("zed-ssh-session")
- .tempdir()?;
- let askpass_delegate = askpass::AskPassDelegate::new(cx, {
- let delegate = delegate.clone();
- move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx)
- });
-
- let mut askpass =
- askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?;
-
- // Start the master SSH process, which does not do anything except for establish
- // the connection and keep it open, allowing other ssh commands to reuse it
- // via a control socket.
- #[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();
-
- let result = select_biased! {
- result = askpass.run().fuse() => {
- match result {
- AskPassResult::CancelledByUser => {
- master_process.kill().ok();
- anyhow::bail!("SSH connection canceled")
- }
- AskPassResult::Timedout => {
- anyhow::bail!("connecting to host timed out")
- }
- }
- }
- _ = stdout.read_to_end(&mut output).fuse() => {
- anyhow::Ok(())
- }
- };
-
- if let Err(e) = result {
- return Err(e.context("Failed to connect to host"));
- }
-
- if master_process.try_status()?.is_some() {
- output.clear();
- let mut stderr = master_process.stderr.take().unwrap();
- stderr.read_to_end(&mut output).await?;
-
- let error_message = format!(
- "failed to connect: {}",
- String::from_utf8_lossy(&output).trim()
- );
- anyhow::bail!(error_message);
- }
-
- #[cfg(not(target_os = "windows"))]
- let socket = SshSocket::new(connection_options, socket_path)?;
- #[cfg(target_os = "windows")]
- let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?;
- drop(askpass);
-
- let ssh_platform = socket.platform().await?;
- let ssh_path_style = match ssh_platform.os {
- "windows" => PathStyle::Windows,
- _ => PathStyle::Posix,
- };
- let ssh_shell = socket.shell().await;
-
- let mut this = Self {
- socket,
- master_process: Mutex::new(Some(master_process)),
- _temp_dir: temp_dir,
- remote_binary_path: None,
- ssh_path_style,
- ssh_platform,
- ssh_shell,
- };
-
- let (release_channel, version, commit) = cx.update(|cx| {
- (
- ReleaseChannel::global(cx),
- AppVersion::global(cx),
- AppCommitSha::try_global(cx),
- )
- })?;
- this.remote_binary_path = Some(
- this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
- .await?,
- );
-
- Ok(this)
- }
-
- fn multiplex(
- mut ssh_proxy_process: Child,
- incoming_tx: UnboundedSender<Envelope>,
- mut outgoing_rx: UnboundedReceiver<Envelope>,
- mut connection_activity_tx: Sender<()>,
- cx: &AsyncApp,
- ) -> Task<Result<i32>> {
- let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
- let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
- let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
-
- let mut stdin_buffer = Vec::new();
- let mut stdout_buffer = Vec::new();
- let mut stderr_buffer = Vec::new();
- let mut stderr_offset = 0;
-
- let stdin_task = cx.background_spawn(async move {
- while let Some(outgoing) = outgoing_rx.next().await {
- write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
- }
- anyhow::Ok(())
- });
-
- let stdout_task = cx.background_spawn({
- let mut connection_activity_tx = connection_activity_tx.clone();
- async move {
- loop {
- stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
- let len = child_stdout.read(&mut stdout_buffer).await?;
-
- if len == 0 {
- return anyhow::Ok(());
- }
-
- if len < MESSAGE_LEN_SIZE {
- child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
- }
-
- let message_len = message_len_from_buffer(&stdout_buffer);
- let envelope =
- read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
- .await?;
- connection_activity_tx.try_send(()).ok();
- incoming_tx.unbounded_send(envelope).ok();
- }
- }
- });
-
- let stderr_task: Task<anyhow::Result<()>> = cx.background_spawn(async move {
- loop {
- stderr_buffer.resize(stderr_offset + 1024, 0);
-
- let len = child_stderr
- .read(&mut stderr_buffer[stderr_offset..])
- .await?;
- if len == 0 {
- return anyhow::Ok(());
- }
-
- stderr_offset += len;
- let mut start_ix = 0;
- while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
- .iter()
- .position(|b| b == &b'\n')
- {
- let line_ix = start_ix + ix;
- let content = &stderr_buffer[start_ix..line_ix];
- start_ix = line_ix + 1;
- if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
- record.log(log::logger())
- } else {
- eprintln!("(remote) {}", String::from_utf8_lossy(content));
- }
- }
- stderr_buffer.drain(0..start_ix);
- stderr_offset -= start_ix;
-
- connection_activity_tx.try_send(()).ok();
- }
- });
-
- cx.background_spawn(async move {
- let result = futures::select! {
- result = stdin_task.fuse() => {
- result.context("stdin")
- }
- result = stdout_task.fuse() => {
- result.context("stdout")
- }
- result = stderr_task.fuse() => {
- result.context("stderr")
- }
- };
-
- let status = ssh_proxy_process.status().await?.code().unwrap_or(1);
- match result {
- Ok(_) => Ok(status),
- Err(error) => Err(error),
- }
- })
- }
-
- #[allow(unused)]
- async fn ensure_server_binary(
- &self,
- delegate: &Arc<dyn SshClientDelegate>,
- release_channel: ReleaseChannel,
- version: SemanticVersion,
- commit: Option<AppCommitSha>,
- cx: &mut AsyncApp,
- ) -> Result<RemotePathBuf> {
- let version_str = match release_channel {
- ReleaseChannel::Nightly => {
- let commit = commit.map(|s| s.full()).unwrap_or_default();
- format!("{}-{}", version, commit)
- }
- ReleaseChannel::Dev => "build".to_string(),
- _ => version.to_string(),
- };
- let binary_name = format!(
- "zed-remote-server-{}-{}",
- release_channel.dev_name(),
- version_str
- );
- let dst_path = RemotePathBuf::new(
- paths::remote_server_dir_relative().join(binary_name),
- self.ssh_path_style,
- );
-
- let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
- #[cfg(debug_assertions)]
- if let Some(build_remote_server) = build_remote_server {
- let src_path = self.build_local(build_remote_server, delegate, cx).await?;
- let tmp_path = RemotePathBuf::new(
- paths::remote_server_dir_relative().join(format!(
- "download-{}-{}",
- std::process::id(),
- src_path.file_name().unwrap().to_string_lossy()
- )),
- self.ssh_path_style,
- );
- self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
- .await?;
- self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
- .await?;
- return Ok(dst_path);
- }
-
- if self
- .socket
- .run_command(&dst_path.to_string(), &["version"])
- .await
- .is_ok()
- {
- return Ok(dst_path);
- }
-
- let wanted_version = cx.update(|cx| match release_channel {
- ReleaseChannel::Nightly => Ok(None),
- ReleaseChannel::Dev => {
- anyhow::bail!(
- "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})",
- dst_path
- )
- }
- _ => Ok(Some(AppVersion::global(cx))),
- })??;
-
- let tmp_path_gz = RemotePathBuf::new(
- PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())),
- self.ssh_path_style,
- );
- if !self.socket.connection_options.upload_binary_over_ssh
- && let Some((url, body)) = delegate
- .get_download_params(self.ssh_platform, release_channel, wanted_version, cx)
- .await?
- {
- match self
- .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx)
- .await
- {
- Ok(_) => {
- self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
- .await?;
- return Ok(dst_path);
- }
- Err(e) => {
- log::error!(
- "Failed to download binary on server, attempting to upload server: {}",
- e
- )
- }
- }
- }
-
- let src_path = delegate
- .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx)
- .await?;
- self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
- .await?;
- self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
- .await?;
- Ok(dst_path)
- }
-
- async fn download_binary_on_server(
- &self,
- url: &str,
- body: &str,
- tmp_path_gz: &RemotePathBuf,
- delegate: &Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Result<()> {
- if let Some(parent) = tmp_path_gz.parent() {
- self.socket
- .run_command(
- "sh",
- &[
- "-lc",
- &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
- ],
- )
- .await?;
- }
-
- delegate.set_status(Some("Downloading remote development server on host"), cx);
-
- match self
- .socket
- .run_command(
- "curl",
- &[
- "-f",
- "-L",
- "-X",
- "GET",
- "-H",
- "Content-Type: application/json",
- "-d",
- body,
- url,
- "-o",
- &tmp_path_gz.to_string(),
- ],
- )
- .await
- {
- Ok(_) => {}
- Err(e) => {
- if self.socket.run_command("which", &["curl"]).await.is_ok() {
- return Err(e);
- }
-
- match self
- .socket
- .run_command(
- "wget",
- &[
- "--method=GET",
- "--header=Content-Type: application/json",
- "--body-data",
- body,
- url,
- "-O",
- &tmp_path_gz.to_string(),
- ],
- )
- .await
- {
- Ok(_) => {}
- Err(e) => {
- if self.socket.run_command("which", &["wget"]).await.is_ok() {
- return Err(e);
- } else {
- anyhow::bail!("Neither curl nor wget is available");
- }
- }
- }
- }
- }
-
- Ok(())
- }
-
- async fn upload_local_server_binary(
- &self,
- src_path: &Path,
- tmp_path_gz: &RemotePathBuf,
- delegate: &Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Result<()> {
- if let Some(parent) = tmp_path_gz.parent() {
- self.socket
- .run_command(
- "sh",
- &[
- "-lc",
- &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
- ],
- )
- .await?;
- }
-
- let src_stat = fs::metadata(&src_path).await?;
- let size = src_stat.len();
-
- let t0 = Instant::now();
- delegate.set_status(Some("Uploading remote development server"), cx);
- log::info!(
- "uploading remote development server to {:?} ({}kb)",
- tmp_path_gz,
- size / 1024
- );
- self.upload_file(src_path, tmp_path_gz)
- .await
- .context("failed to upload server binary")?;
- log::info!("uploaded remote development server in {:?}", t0.elapsed());
- Ok(())
- }
-
- async fn extract_server_binary(
- &self,
- dst_path: &RemotePathBuf,
- tmp_path: &RemotePathBuf,
- delegate: &Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Result<()> {
- delegate.set_status(Some("Extracting remote development server"), cx);
- let server_mode = 0o755;
-
- let orig_tmp_path = tmp_path.to_string();
- let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
- shell_script!(
- "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
- server_mode = &format!("{:o}", server_mode),
- dst_path = &dst_path.to_string(),
- )
- } else {
- shell_script!(
- "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
- server_mode = &format!("{:o}", server_mode),
- dst_path = &dst_path.to_string()
- )
- };
- self.socket.run_command("sh", &["-lc", &script]).await?;
- Ok(())
- }
-
- async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> {
- log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
- let mut command = util::command::new_smol_command("scp");
- let output = self
- .socket
- .ssh_options(&mut command)
- .args(
- self.socket
- .connection_options
- .port
- .map(|port| vec!["-P".to_string(), port.to_string()])
- .unwrap_or_default(),
- )
- .arg(src_path)
- .arg(format!(
- "{}:{}",
- self.socket.connection_options.scp_url(),
- dest_path
- ))
- .output()
- .await?;
-
- anyhow::ensure!(
- output.status.success(),
- "failed to upload file {} -> {}: {}",
- src_path.display(),
- dest_path.to_string(),
- String::from_utf8_lossy(&output.stderr)
- );
- Ok(())
- }
-
- #[cfg(debug_assertions)]
- async fn build_local(
- &self,
- build_remote_server: String,
- delegate: &Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Result<PathBuf> {
- use smol::process::{Command, Stdio};
- use std::env::VarError;
-
- async fn run_cmd(command: &mut Command) -> Result<()> {
- let output = command
- .kill_on_drop(true)
- .stderr(Stdio::inherit())
- .output()
- .await?;
- anyhow::ensure!(
- output.status.success(),
- "Failed to run command: {command:?}"
- );
- Ok(())
- }
-
- let use_musl = !build_remote_server.contains("nomusl");
- let triple = format!(
- "{}-{}",
- self.ssh_platform.arch,
- match self.ssh_platform.os {
- "linux" =>
- if use_musl {
- "unknown-linux-musl"
- } else {
- "unknown-linux-gnu"
- },
- "macos" => "apple-darwin",
- _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform),
- }
- );
- let mut rust_flags = match std::env::var("RUSTFLAGS") {
- Ok(val) => val,
- Err(VarError::NotPresent) => String::new(),
- Err(e) => {
- log::error!("Failed to get env var `RUSTFLAGS` value: {e}");
- String::new()
- }
- };
- if self.ssh_platform.os == "linux" && use_musl {
- rust_flags.push_str(" -C target-feature=+crt-static");
- }
- if build_remote_server.contains("mold") {
- rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
- }
-
- if self.ssh_platform.arch == std::env::consts::ARCH
- && self.ssh_platform.os == std::env::consts::OS
- {
- delegate.set_status(Some("Building remote server binary from source"), cx);
- log::info!("building remote server binary from source");
- run_cmd(
- Command::new("cargo")
- .args([
- "build",
- "--package",
- "remote_server",
- "--features",
- "debug-embed",
- "--target-dir",
- "target/remote_server",
- "--target",
- &triple,
- ])
- .env("RUSTFLAGS", &rust_flags),
- )
- .await?;
- } else if build_remote_server.contains("cross") {
- #[cfg(target_os = "windows")]
- use util::paths::SanitizedPath;
-
- delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
- log::info!("installing cross");
- run_cmd(Command::new("cargo").args([
- "install",
- "cross",
- "--git",
- "https://github.com/cross-rs/cross",
- ]))
- .await?;
-
- delegate.set_status(
- Some(&format!(
- "Building remote server binary from source for {} with Docker",
- &triple
- )),
- cx,
- );
- log::info!("building remote server binary from source for {}", &triple);
-
- // On Windows, the binding needs to be set to the canonical path
- #[cfg(target_os = "windows")]
- let src =
- SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string();
- #[cfg(not(target_os = "windows"))]
- let src = "./target";
- run_cmd(
- Command::new("cross")
- .args([
- "build",
- "--package",
- "remote_server",
- "--features",
- "debug-embed",
- "--target-dir",
- "target/remote_server",
- "--target",
- &triple,
- ])
- .env(
- "CROSS_CONTAINER_OPTS",
- format!("--mount type=bind,src={src},dst=/app/target"),
- )
- .env("RUSTFLAGS", &rust_flags),
- )
- .await?;
- } else {
- let which = cx
- .background_spawn(async move { which::which("zig") })
- .await;
-
- if which.is_err() {
- #[cfg(not(target_os = "windows"))]
- {
- anyhow::bail!(
- "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
- )
- }
- #[cfg(target_os = "windows")]
- {
- anyhow::bail!(
- "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
- )
- }
- }
-
- delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
- log::info!("adding rustup target");
- run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
-
- delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
- log::info!("installing cargo-zigbuild");
- run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
-
- delegate.set_status(
- Some(&format!(
- "Building remote binary from source for {triple} with Zig"
- )),
- cx,
- );
- log::info!("building remote binary from source for {triple} with Zig");
- run_cmd(
- Command::new("cargo")
- .args([
- "zigbuild",
- "--package",
- "remote_server",
- "--features",
- "debug-embed",
- "--target-dir",
- "target/remote_server",
- "--target",
- &triple,
- ])
- .env("RUSTFLAGS", &rust_flags),
- )
- .await?;
- };
- let bin_path = Path::new("target")
- .join("remote_server")
- .join(&triple)
- .join("debug")
- .join("remote_server");
-
- let path = if !build_remote_server.contains("nocompress") {
- delegate.set_status(Some("Compressing binary"), cx);
-
- #[cfg(not(target_os = "windows"))]
- {
- run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
- }
- #[cfg(target_os = "windows")]
- {
- // On Windows, we use 7z to compress the binary
- let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
- let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
- if smol::fs::metadata(&gz_path).await.is_ok() {
- smol::fs::remove_file(&gz_path).await?;
- }
- run_cmd(Command::new(seven_zip).args([
- "a",
- "-tgzip",
- &gz_path,
- &bin_path.to_string_lossy(),
- ]))
- .await?;
- }
-
- let mut archive_path = bin_path;
- archive_path.set_extension("gz");
- std::env::current_dir()?.join(archive_path)
- } else {
- bin_path
- };
-
- Ok(path)
- }
-}
-
-type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
-
-struct ChannelClient {
- next_message_id: AtomicU32,
- outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>,
- buffer: Mutex<VecDeque<Envelope>>,
- response_channels: ResponseChannels,
- message_handlers: Mutex<ProtoMessageHandlerSet>,
- max_received: AtomicU32,
- name: &'static str,
- task: Mutex<Task<Result<()>>>,
-}
-
-impl ChannelClient {
- fn new(
- incoming_rx: mpsc::UnboundedReceiver<Envelope>,
- outgoing_tx: mpsc::UnboundedSender<Envelope>,
- cx: &App,
- name: &'static str,
- ) -> Arc<Self> {
- Arc::new_cyclic(|this| Self {
- outgoing_tx: Mutex::new(outgoing_tx),
- next_message_id: AtomicU32::new(0),
- max_received: AtomicU32::new(0),
- response_channels: ResponseChannels::default(),
- message_handlers: Default::default(),
- buffer: Mutex::new(VecDeque::new()),
- name,
- task: Mutex::new(Self::start_handling_messages(
- this.clone(),
- incoming_rx,
- &cx.to_async(),
- )),
- })
- }
-
- fn start_handling_messages(
- this: Weak<Self>,
- mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
- cx: &AsyncApp,
- ) -> Task<Result<()>> {
- cx.spawn(async move |cx| {
- let peer_id = PeerId { owner_id: 0, id: 0 };
- while let Some(incoming) = incoming_rx.next().await {
- let Some(this) = this.upgrade() else {
- return anyhow::Ok(());
- };
- if let Some(ack_id) = incoming.ack_id {
- let mut buffer = this.buffer.lock();
- while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
- buffer.pop_front();
- }
- }
- if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload
- {
- log::debug!(
- "{}:ssh message received. name:FlushBufferedMessages",
- this.name
- );
- {
- let buffer = this.buffer.lock();
- for envelope in buffer.iter() {
- this.outgoing_tx
- .lock()
- .unbounded_send(envelope.clone())
- .ok();
- }
- }
- let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None);
- envelope.id = this.next_message_id.fetch_add(1, SeqCst);
- this.outgoing_tx.lock().unbounded_send(envelope).ok();
- continue;
- }
-
- this.max_received.store(incoming.id, SeqCst);
-
- if let Some(request_id) = incoming.responding_to {
- let request_id = MessageId(request_id);
- let sender = this.response_channels.lock().remove(&request_id);
- if let Some(sender) = sender {
- let (tx, rx) = oneshot::channel();
- if incoming.payload.is_some() {
- sender.send((incoming, tx)).ok();
- }
- rx.await.ok();
- }
- } else if let Some(envelope) =
- build_typed_envelope(peer_id, Instant::now(), incoming)
- {
- let type_name = envelope.payload_type_name();
- let message_id = envelope.message_id();
- if let Some(future) = ProtoMessageHandlerSet::handle_message(
- &this.message_handlers,
- envelope,
- this.clone().into(),
- cx.clone(),
- ) {
- log::debug!("{}:ssh message received. name:{type_name}", this.name);
- cx.foreground_executor()
- .spawn(async move {
- match future.await {
- Ok(_) => {
- log::debug!(
- "{}:ssh message handled. name:{type_name}",
- this.name
- );
- }
- Err(error) => {
- log::error!(
- "{}:error handling message. type:{}, error:{}",
- this.name,
- type_name,
- format!("{error:#}").lines().fold(
- String::new(),
- |mut message, line| {
- if !message.is_empty() {
- message.push(' ');
- }
- message.push_str(line);
- message
- }
- )
- );
- }
- }
- })
- .detach()
- } else {
- log::error!("{}:unhandled ssh message name:{type_name}", this.name);
- if let Err(e) = AnyProtoClient::from(this.clone()).send_response(
- message_id,
- anyhow::anyhow!("no handler registered for {type_name}").to_proto(),
- ) {
- log::error!(
- "{}:error sending error response for {type_name}:{e:#}",
- this.name
- );
- }
- }
- }
- }
- anyhow::Ok(())
- })
- }
-
- fn reconnect(
- self: &Arc<Self>,
- incoming_rx: UnboundedReceiver<Envelope>,
- outgoing_tx: UnboundedSender<Envelope>,
- cx: &AsyncApp,
- ) {
- *self.outgoing_tx.lock() = outgoing_tx;
- *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx);
- }
-
- fn request<T: RequestMessage>(
- &self,
- payload: T,
- ) -> impl 'static + Future<Output = Result<T::Response>> {
- self.request_internal(payload, true)
- }
-
- fn request_internal<T: RequestMessage>(
- &self,
- payload: T,
- use_buffer: bool,
- ) -> impl 'static + Future<Output = Result<T::Response>> {
- log::debug!("ssh request start. name:{}", T::NAME);
- let response =
- self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer);
- async move {
- let response = response.await?;
- log::debug!("ssh request finish. name:{}", T::NAME);
- T::Response::from_envelope(response).context("received a response of the wrong type")
- }
- }
-
- async fn resync(&self, timeout: Duration) -> Result<()> {
- smol::future::or(
- async {
- self.request_internal(proto::FlushBufferedMessages {}, false)
- .await?;
-
- for envelope in self.buffer.lock().iter() {
- self.outgoing_tx
- .lock()
- .unbounded_send(envelope.clone())
- .ok();
- }
- Ok(())
- },
- async {
- smol::Timer::after(timeout).await;
- anyhow::bail!("Timed out resyncing remote client")
- },
- )
- .await
- }
-
- async fn ping(&self, timeout: Duration) -> Result<()> {
- smol::future::or(
- async {
- self.request(proto::Ping {}).await?;
- Ok(())
- },
- async {
- smol::Timer::after(timeout).await;
- anyhow::bail!("Timed out pinging remote client")
- },
- )
- .await
- }
-
- pub fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
- log::debug!("ssh send name:{}", T::NAME);
- self.send_dynamic(payload.into_envelope(0, None, None))
- }
-
- fn request_dynamic(
- &self,
- mut envelope: proto::Envelope,
- type_name: &'static str,
- use_buffer: bool,
- ) -> impl 'static + Future<Output = Result<proto::Envelope>> {
- envelope.id = self.next_message_id.fetch_add(1, SeqCst);
- let (tx, rx) = oneshot::channel();
- let mut response_channels_lock = self.response_channels.lock();
- response_channels_lock.insert(MessageId(envelope.id), tx);
- drop(response_channels_lock);
-
- let result = if use_buffer {
- self.send_buffered(envelope)
- } else {
- self.send_unbuffered(envelope)
- };
- async move {
- if let Err(error) = &result {
- log::error!("failed to send message: {error}");
- anyhow::bail!("failed to send message: {error}");
- }
-
- let response = rx.await.context("connection lost")?.0;
- if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
- return Err(RpcError::from_proto(error, type_name));
- }
- Ok(response)
- }
- }
-
- pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
- envelope.id = self.next_message_id.fetch_add(1, SeqCst);
- self.send_buffered(envelope)
- }
-
- fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
- envelope.ack_id = Some(self.max_received.load(SeqCst));
- self.buffer.lock().push_back(envelope.clone());
- // ignore errors on send (happen while we're reconnecting)
- // assume that the global "disconnected" overlay is sufficient.
- self.outgoing_tx.lock().unbounded_send(envelope).ok();
- Ok(())
- }
-
- fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> {
- envelope.ack_id = Some(self.max_received.load(SeqCst));
- self.outgoing_tx.lock().unbounded_send(envelope).ok();
- Ok(())
- }
-}
-
-impl ProtoClient for ChannelClient {
- fn request(
- &self,
- envelope: proto::Envelope,
- request_type: &'static str,
- ) -> BoxFuture<'static, Result<proto::Envelope>> {
- self.request_dynamic(envelope, request_type, true).boxed()
- }
-
- fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
- self.send_dynamic(envelope)
- }
-
- fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> {
- self.send_dynamic(envelope)
- }
-
- fn message_handler_set(&self) -> &Mutex<ProtoMessageHandlerSet> {
- &self.message_handlers
- }
-
- fn is_via_collab(&self) -> bool {
- false
- }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-mod fake {
- use std::{path::PathBuf, sync::Arc};
-
- use anyhow::Result;
- use async_trait::async_trait;
- use futures::{
- FutureExt, SinkExt, StreamExt,
- channel::{
- mpsc::{self, Sender},
- oneshot,
- },
- select_biased,
- };
- use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext};
- use release_channel::ReleaseChannel;
- use rpc::proto::Envelope;
- use util::paths::{PathStyle, RemotePathBuf};
-
- use super::{
- ChannelClient, RemoteConnection, SshArgs, SshClientDelegate, SshConnectionOptions,
- SshPlatform,
- };
-
- pub(super) struct FakeRemoteConnection {
- pub(super) connection_options: SshConnectionOptions,
- pub(super) server_channel: Arc<ChannelClient>,
- pub(super) server_cx: SendableCx,
- }
-
- pub(super) struct SendableCx(AsyncApp);
- impl SendableCx {
- // SAFETY: When run in test mode, GPUI is always single threaded.
- pub(super) fn new(cx: &TestAppContext) -> Self {
- Self(cx.to_async())
- }
-
- // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp
- fn get(&self, _: &AsyncApp) -> AsyncApp {
- self.0.clone()
- }
- }
-
- // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`]
- unsafe impl Send for SendableCx {}
- unsafe impl Sync for SendableCx {}
-
- #[async_trait(?Send)]
- impl RemoteConnection for FakeRemoteConnection {
- async fn kill(&self) -> Result<()> {
- Ok(())
- }
-
- fn has_been_killed(&self) -> bool {
- false
- }
-
- fn ssh_args(&self) -> SshArgs {
- SshArgs {
- arguments: Vec::new(),
- envs: None,
- }
- }
-
- fn upload_directory(
- &self,
- _src_path: PathBuf,
- _dest_path: RemotePathBuf,
- _cx: &App,
- ) -> Task<Result<()>> {
- unreachable!()
- }
-
- fn connection_options(&self) -> SshConnectionOptions {
- self.connection_options.clone()
- }
-
- fn simulate_disconnect(&self, cx: &AsyncApp) {
- let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
- let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
- self.server_channel
- .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
- }
-
- fn start_proxy(
- &self,
- _unique_identifier: String,
- _reconnect: bool,
- mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
- mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
- mut connection_activity_tx: Sender<()>,
- _delegate: Arc<dyn SshClientDelegate>,
- cx: &mut AsyncApp,
- ) -> Task<Result<i32>> {
- let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
- let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
-
- self.server_channel.reconnect(
- server_incoming_rx,
- server_outgoing_tx,
- &self.server_cx.get(cx),
- );
-
- cx.background_spawn(async move {
- loop {
- select_biased! {
- server_to_client = server_outgoing_rx.next().fuse() => {
- let Some(server_to_client) = server_to_client else {
- return Ok(1)
- };
- connection_activity_tx.try_send(()).ok();
- client_incoming_tx.send(server_to_client).await.ok();
- }
- client_to_server = client_outgoing_rx.next().fuse() => {
- let Some(client_to_server) = client_to_server else {
- return Ok(1)
- };
- server_incoming_tx.send(client_to_server).await.ok();
- }
- }
- }
- })
- }
-
- fn path_style(&self) -> PathStyle {
- PathStyle::current()
- }
-
- fn shell(&self) -> String {
- "sh".to_owned()
- }
- }
-
- pub(super) struct Delegate;
-
- impl SshClientDelegate for Delegate {
- fn ask_password(&self, _: String, _: oneshot::Sender<String>, _: &mut AsyncApp) {
- unreachable!()
- }
-
- fn download_server_binary_locally(
- &self,
- _: SshPlatform,
- _: ReleaseChannel,
- _: Option<SemanticVersion>,
- _: &mut AsyncApp,
- ) -> Task<Result<PathBuf>> {
- unreachable!()
- }
-
- fn get_download_params(
- &self,
- _platform: SshPlatform,
- _release_channel: ReleaseChannel,
- _version: Option<SemanticVersion>,
- _cx: &mut AsyncApp,
- ) -> Task<Result<Option<(String, String)>>> {
- unreachable!()
- }
-
- fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {}
- }
-}
@@ -0,0 +1 @@
+pub mod ssh;
@@ -0,0 +1,1358 @@
+use crate::{
+ RemoteClientDelegate, RemotePlatform,
+ json_log::LogRecord,
+ protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
+ remote_client::{CommandTemplate, RemoteConnection},
+};
+use anyhow::{Context as _, Result, anyhow};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::{
+ AsyncReadExt as _, FutureExt as _, StreamExt as _,
+ channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender},
+ select_biased,
+};
+use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task};
+use itertools::Itertools;
+use parking_lot::Mutex;
+use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
+use rpc::proto::Envelope;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use smol::{
+ fs,
+ process::{self, Child, Stdio},
+};
+use std::{
+ iter,
+ path::{Path, PathBuf},
+ sync::Arc,
+ time::Instant,
+};
+use tempfile::TempDir;
+use util::paths::{PathStyle, RemotePathBuf};
+
+pub(crate) struct SshRemoteConnection {
+ socket: SshSocket,
+ master_process: Mutex<Option<Child>>,
+ remote_binary_path: Option<RemotePathBuf>,
+ ssh_platform: RemotePlatform,
+ ssh_path_style: PathStyle,
+ ssh_shell: String,
+ _temp_dir: TempDir,
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
+pub struct SshConnectionOptions {
+ pub host: String,
+ pub username: Option<String>,
+ pub port: Option<u16>,
+ pub password: Option<String>,
+ pub args: Option<Vec<String>>,
+ pub port_forwards: Option<Vec<SshPortForwardOption>>,
+
+ pub nickname: Option<String>,
+ pub upload_binary_over_ssh: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
+pub struct SshPortForwardOption {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub local_host: Option<String>,
+ pub local_port: u16,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub remote_host: Option<String>,
+ pub remote_port: u16,
+}
+
+#[derive(Clone)]
+struct SshSocket {
+ connection_options: SshConnectionOptions,
+ #[cfg(not(target_os = "windows"))]
+ socket_path: PathBuf,
+ envs: HashMap<String, String>,
+}
+
+macro_rules! shell_script {
+ ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
+ format!(
+ $fmt,
+ $(
+ $name = shlex::try_quote($arg).unwrap()
+ ),+
+ )
+ }};
+}
+
+#[async_trait(?Send)]
+impl RemoteConnection for SshRemoteConnection {
+ async fn kill(&self) -> Result<()> {
+ let Some(mut process) = self.master_process.lock().take() else {
+ return Ok(());
+ };
+ process.kill().ok();
+ process.status().await?;
+ Ok(())
+ }
+
+ fn has_been_killed(&self) -> bool {
+ self.master_process.lock().is_none()
+ }
+
+ fn connection_options(&self) -> SshConnectionOptions {
+ self.socket.connection_options.clone()
+ }
+
+ fn shell(&self) -> String {
+ self.ssh_shell.clone()
+ }
+
+ fn build_command(
+ &self,
+ input_program: Option<String>,
+ input_args: &[String],
+ input_env: &HashMap<String, String>,
+ 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 = "~/";
+ if working_dir.starts_with(TILDE_PREFIX) {
+ let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/");
+ write!(&mut script, "cd \"$HOME/{working_dir}\"; ").unwrap();
+ } else {
+ 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(),
+ })
+ }
+
+ fn upload_directory(
+ &self,
+ src_path: PathBuf,
+ dest_path: RemotePathBuf,
+ cx: &App,
+ ) -> Task<Result<()>> {
+ let mut command = util::command::new_smol_command("scp");
+ let output = self
+ .socket
+ .ssh_options(&mut command)
+ .args(
+ self.socket
+ .connection_options
+ .port
+ .map(|port| vec!["-P".to_string(), port.to_string()])
+ .unwrap_or_default(),
+ )
+ .arg("-C")
+ .arg("-r")
+ .arg(&src_path)
+ .arg(format!(
+ "{}:{}",
+ self.socket.connection_options.scp_url(),
+ dest_path
+ ))
+ .output();
+
+ cx.background_spawn(async move {
+ let output = output.await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "failed to upload directory {} -> {}: {}",
+ src_path.display(),
+ dest_path.to_string(),
+ String::from_utf8_lossy(&output.stderr)
+ );
+
+ Ok(())
+ })
+ }
+
+ fn start_proxy(
+ &self,
+ unique_identifier: String,
+ reconnect: bool,
+ incoming_tx: UnboundedSender<Envelope>,
+ outgoing_rx: UnboundedReceiver<Envelope>,
+ connection_activity_tx: Sender<()>,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<i32>> {
+ delegate.set_status(Some("Starting proxy"), cx);
+
+ let Some(remote_binary_path) = self.remote_binary_path.clone() else {
+ return Task::ready(Err(anyhow!("Remote binary path not set")));
+ };
+
+ let mut start_proxy_command = shell_script!(
+ "exec {binary_path} proxy --identifier {identifier}",
+ binary_path = &remote_binary_path.to_string(),
+ identifier = &unique_identifier,
+ );
+
+ for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
+ if let Some(value) = std::env::var(env_var).ok() {
+ start_proxy_command = format!(
+ "{}={} {} ",
+ env_var,
+ shlex::try_quote(&value).unwrap(),
+ start_proxy_command,
+ );
+ }
+ }
+
+ if reconnect {
+ start_proxy_command.push_str(" --reconnect");
+ }
+
+ let ssh_proxy_process = match self
+ .socket
+ .ssh_command("sh", &["-lc", &start_proxy_command])
+ // IMPORTANT: we kill this process when we drop the task that uses it.
+ .kill_on_drop(true)
+ .spawn()
+ {
+ Ok(process) => process,
+ Err(error) => {
+ return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error)));
+ }
+ };
+
+ Self::multiplex(
+ ssh_proxy_process,
+ incoming_tx,
+ outgoing_rx,
+ connection_activity_tx,
+ cx,
+ )
+ }
+
+ fn path_style(&self) -> PathStyle {
+ self.ssh_path_style
+ }
+}
+
+impl SshRemoteConnection {
+ pub(crate) async fn new(
+ connection_options: SshConnectionOptions,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Result<Self> {
+ use askpass::AskPassResult;
+
+ delegate.set_status(Some("Connecting"), cx);
+
+ let url = connection_options.ssh_url();
+
+ let temp_dir = tempfile::Builder::new()
+ .prefix("zed-ssh-session")
+ .tempdir()?;
+ let askpass_delegate = askpass::AskPassDelegate::new(cx, {
+ let delegate = delegate.clone();
+ move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx)
+ });
+
+ let mut askpass =
+ askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?;
+
+ // Start the master SSH process, which does not do anything except for establish
+ // the connection and keep it open, allowing other ssh commands to reuse it
+ // via a control socket.
+ #[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();
+
+ let result = select_biased! {
+ result = askpass.run().fuse() => {
+ match result {
+ AskPassResult::CancelledByUser => {
+ master_process.kill().ok();
+ anyhow::bail!("SSH connection canceled")
+ }
+ AskPassResult::Timedout => {
+ anyhow::bail!("connecting to host timed out")
+ }
+ }
+ }
+ _ = stdout.read_to_end(&mut output).fuse() => {
+ anyhow::Ok(())
+ }
+ };
+
+ if let Err(e) = result {
+ return Err(e.context("Failed to connect to host"));
+ }
+
+ if master_process.try_status()?.is_some() {
+ output.clear();
+ let mut stderr = master_process.stderr.take().unwrap();
+ stderr.read_to_end(&mut output).await?;
+
+ let error_message = format!(
+ "failed to connect: {}",
+ String::from_utf8_lossy(&output).trim()
+ );
+ anyhow::bail!(error_message);
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ let socket = SshSocket::new(connection_options, socket_path)?;
+ #[cfg(target_os = "windows")]
+ let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?;
+ drop(askpass);
+
+ let ssh_platform = socket.platform().await?;
+ let ssh_path_style = match ssh_platform.os {
+ "windows" => PathStyle::Windows,
+ _ => PathStyle::Posix,
+ };
+ let ssh_shell = socket.shell().await;
+
+ let mut this = Self {
+ socket,
+ master_process: Mutex::new(Some(master_process)),
+ _temp_dir: temp_dir,
+ remote_binary_path: None,
+ ssh_path_style,
+ ssh_platform,
+ ssh_shell,
+ };
+
+ let (release_channel, version, commit) = cx.update(|cx| {
+ (
+ ReleaseChannel::global(cx),
+ AppVersion::global(cx),
+ AppCommitSha::try_global(cx),
+ )
+ })?;
+ this.remote_binary_path = Some(
+ this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
+ .await?,
+ );
+
+ Ok(this)
+ }
+
+ fn multiplex(
+ mut ssh_proxy_process: Child,
+ incoming_tx: UnboundedSender<Envelope>,
+ mut outgoing_rx: UnboundedReceiver<Envelope>,
+ mut connection_activity_tx: Sender<()>,
+ cx: &AsyncApp,
+ ) -> Task<Result<i32>> {
+ let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
+ let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
+ let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
+
+ let mut stdin_buffer = Vec::new();
+ let mut stdout_buffer = Vec::new();
+ let mut stderr_buffer = Vec::new();
+ let mut stderr_offset = 0;
+
+ let stdin_task = cx.background_spawn(async move {
+ while let Some(outgoing) = outgoing_rx.next().await {
+ write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
+ }
+ anyhow::Ok(())
+ });
+
+ let stdout_task = cx.background_spawn({
+ let mut connection_activity_tx = connection_activity_tx.clone();
+ async move {
+ loop {
+ stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
+ let len = child_stdout.read(&mut stdout_buffer).await?;
+
+ if len == 0 {
+ return anyhow::Ok(());
+ }
+
+ if len < MESSAGE_LEN_SIZE {
+ child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
+ }
+
+ let message_len = message_len_from_buffer(&stdout_buffer);
+ let envelope =
+ read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
+ .await?;
+ connection_activity_tx.try_send(()).ok();
+ incoming_tx.unbounded_send(envelope).ok();
+ }
+ }
+ });
+
+ let stderr_task: Task<anyhow::Result<()>> = cx.background_spawn(async move {
+ loop {
+ stderr_buffer.resize(stderr_offset + 1024, 0);
+
+ let len = child_stderr
+ .read(&mut stderr_buffer[stderr_offset..])
+ .await?;
+ if len == 0 {
+ return anyhow::Ok(());
+ }
+
+ stderr_offset += len;
+ let mut start_ix = 0;
+ while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
+ .iter()
+ .position(|b| b == &b'\n')
+ {
+ let line_ix = start_ix + ix;
+ let content = &stderr_buffer[start_ix..line_ix];
+ start_ix = line_ix + 1;
+ if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
+ record.log(log::logger())
+ } else {
+ eprintln!("(remote) {}", String::from_utf8_lossy(content));
+ }
+ }
+ stderr_buffer.drain(0..start_ix);
+ stderr_offset -= start_ix;
+
+ connection_activity_tx.try_send(()).ok();
+ }
+ });
+
+ cx.background_spawn(async move {
+ let result = futures::select! {
+ result = stdin_task.fuse() => {
+ result.context("stdin")
+ }
+ result = stdout_task.fuse() => {
+ result.context("stdout")
+ }
+ result = stderr_task.fuse() => {
+ result.context("stderr")
+ }
+ };
+
+ let status = ssh_proxy_process.status().await?.code().unwrap_or(1);
+ match result {
+ Ok(_) => Ok(status),
+ Err(error) => Err(error),
+ }
+ })
+ }
+
+ #[allow(unused)]
+ async fn ensure_server_binary(
+ &self,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ release_channel: ReleaseChannel,
+ version: SemanticVersion,
+ commit: Option<AppCommitSha>,
+ cx: &mut AsyncApp,
+ ) -> Result<RemotePathBuf> {
+ let version_str = match release_channel {
+ ReleaseChannel::Nightly => {
+ let commit = commit.map(|s| s.full()).unwrap_or_default();
+ format!("{}-{}", version, commit)
+ }
+ ReleaseChannel::Dev => "build".to_string(),
+ _ => version.to_string(),
+ };
+ let binary_name = format!(
+ "zed-remote-server-{}-{}",
+ release_channel.dev_name(),
+ version_str
+ );
+ let dst_path = RemotePathBuf::new(
+ paths::remote_server_dir_relative().join(binary_name),
+ self.ssh_path_style,
+ );
+
+ let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
+ #[cfg(debug_assertions)]
+ if let Some(build_remote_server) = build_remote_server {
+ let src_path = self.build_local(build_remote_server, delegate, cx).await?;
+ let tmp_path = RemotePathBuf::new(
+ paths::remote_server_dir_relative().join(format!(
+ "download-{}-{}",
+ std::process::id(),
+ src_path.file_name().unwrap().to_string_lossy()
+ )),
+ self.ssh_path_style,
+ );
+ self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
+ .await?;
+ self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
+ .await?;
+ return Ok(dst_path);
+ }
+
+ if self
+ .socket
+ .run_command(&dst_path.to_string(), &["version"])
+ .await
+ .is_ok()
+ {
+ return Ok(dst_path);
+ }
+
+ let wanted_version = cx.update(|cx| match release_channel {
+ ReleaseChannel::Nightly => Ok(None),
+ ReleaseChannel::Dev => {
+ anyhow::bail!(
+ "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})",
+ dst_path
+ )
+ }
+ _ => Ok(Some(AppVersion::global(cx))),
+ })??;
+
+ let tmp_path_gz = RemotePathBuf::new(
+ PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())),
+ self.ssh_path_style,
+ );
+ if !self.socket.connection_options.upload_binary_over_ssh
+ && let Some((url, body)) = delegate
+ .get_download_params(self.ssh_platform, release_channel, wanted_version, cx)
+ .await?
+ {
+ match self
+ .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx)
+ .await
+ {
+ Ok(_) => {
+ self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
+ .await?;
+ return Ok(dst_path);
+ }
+ Err(e) => {
+ log::error!(
+ "Failed to download binary on server, attempting to upload server: {}",
+ e
+ )
+ }
+ }
+ }
+
+ let src_path = delegate
+ .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx)
+ .await?;
+ self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
+ .await?;
+ self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
+ .await?;
+ Ok(dst_path)
+ }
+
+ async fn download_binary_on_server(
+ &self,
+ url: &str,
+ body: &str,
+ tmp_path_gz: &RemotePathBuf,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Result<()> {
+ if let Some(parent) = tmp_path_gz.parent() {
+ self.socket
+ .run_command(
+ "sh",
+ &[
+ "-lc",
+ &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
+ ],
+ )
+ .await?;
+ }
+
+ delegate.set_status(Some("Downloading remote development server on host"), cx);
+
+ match self
+ .socket
+ .run_command(
+ "curl",
+ &[
+ "-f",
+ "-L",
+ "-X",
+ "GET",
+ "-H",
+ "Content-Type: application/json",
+ "-d",
+ body,
+ url,
+ "-o",
+ &tmp_path_gz.to_string(),
+ ],
+ )
+ .await
+ {
+ Ok(_) => {}
+ Err(e) => {
+ if self.socket.run_command("which", &["curl"]).await.is_ok() {
+ return Err(e);
+ }
+
+ match self
+ .socket
+ .run_command(
+ "wget",
+ &[
+ "--method=GET",
+ "--header=Content-Type: application/json",
+ "--body-data",
+ body,
+ url,
+ "-O",
+ &tmp_path_gz.to_string(),
+ ],
+ )
+ .await
+ {
+ Ok(_) => {}
+ Err(e) => {
+ if self.socket.run_command("which", &["wget"]).await.is_ok() {
+ return Err(e);
+ } else {
+ anyhow::bail!("Neither curl nor wget is available");
+ }
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn upload_local_server_binary(
+ &self,
+ src_path: &Path,
+ tmp_path_gz: &RemotePathBuf,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Result<()> {
+ if let Some(parent) = tmp_path_gz.parent() {
+ self.socket
+ .run_command(
+ "sh",
+ &[
+ "-lc",
+ &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
+ ],
+ )
+ .await?;
+ }
+
+ let src_stat = fs::metadata(&src_path).await?;
+ let size = src_stat.len();
+
+ let t0 = Instant::now();
+ delegate.set_status(Some("Uploading remote development server"), cx);
+ log::info!(
+ "uploading remote development server to {:?} ({}kb)",
+ tmp_path_gz,
+ size / 1024
+ );
+ self.upload_file(src_path, tmp_path_gz)
+ .await
+ .context("failed to upload server binary")?;
+ log::info!("uploaded remote development server in {:?}", t0.elapsed());
+ Ok(())
+ }
+
+ async fn extract_server_binary(
+ &self,
+ dst_path: &RemotePathBuf,
+ tmp_path: &RemotePathBuf,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Result<()> {
+ delegate.set_status(Some("Extracting remote development server"), cx);
+ let server_mode = 0o755;
+
+ let orig_tmp_path = tmp_path.to_string();
+ let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
+ shell_script!(
+ "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
+ server_mode = &format!("{:o}", server_mode),
+ dst_path = &dst_path.to_string(),
+ )
+ } else {
+ shell_script!(
+ "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
+ server_mode = &format!("{:o}", server_mode),
+ dst_path = &dst_path.to_string()
+ )
+ };
+ self.socket.run_command("sh", &["-lc", &script]).await?;
+ Ok(())
+ }
+
+ async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> {
+ log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
+ let mut command = util::command::new_smol_command("scp");
+ let output = self
+ .socket
+ .ssh_options(&mut command)
+ .args(
+ self.socket
+ .connection_options
+ .port
+ .map(|port| vec!["-P".to_string(), port.to_string()])
+ .unwrap_or_default(),
+ )
+ .arg(src_path)
+ .arg(format!(
+ "{}:{}",
+ self.socket.connection_options.scp_url(),
+ dest_path
+ ))
+ .output()
+ .await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "failed to upload file {} -> {}: {}",
+ src_path.display(),
+ dest_path.to_string(),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ Ok(())
+ }
+
+ #[cfg(debug_assertions)]
+ async fn build_local(
+ &self,
+ build_remote_server: String,
+ delegate: &Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+ ) -> Result<PathBuf> {
+ use smol::process::{Command, Stdio};
+ use std::env::VarError;
+
+ async fn run_cmd(command: &mut Command) -> Result<()> {
+ let output = command
+ .kill_on_drop(true)
+ .stderr(Stdio::inherit())
+ .output()
+ .await?;
+ anyhow::ensure!(
+ output.status.success(),
+ "Failed to run command: {command:?}"
+ );
+ Ok(())
+ }
+
+ let use_musl = !build_remote_server.contains("nomusl");
+ let triple = format!(
+ "{}-{}",
+ self.ssh_platform.arch,
+ match self.ssh_platform.os {
+ "linux" =>
+ if use_musl {
+ "unknown-linux-musl"
+ } else {
+ "unknown-linux-gnu"
+ },
+ "macos" => "apple-darwin",
+ _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform),
+ }
+ );
+ let mut rust_flags = match std::env::var("RUSTFLAGS") {
+ Ok(val) => val,
+ Err(VarError::NotPresent) => String::new(),
+ Err(e) => {
+ log::error!("Failed to get env var `RUSTFLAGS` value: {e}");
+ String::new()
+ }
+ };
+ if self.ssh_platform.os == "linux" && use_musl {
+ rust_flags.push_str(" -C target-feature=+crt-static");
+ }
+ if build_remote_server.contains("mold") {
+ rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
+ }
+
+ if self.ssh_platform.arch == std::env::consts::ARCH
+ && self.ssh_platform.os == std::env::consts::OS
+ {
+ delegate.set_status(Some("Building remote server binary from source"), cx);
+ log::info!("building remote server binary from source");
+ run_cmd(
+ Command::new("cargo")
+ .args([
+ "build",
+ "--package",
+ "remote_server",
+ "--features",
+ "debug-embed",
+ "--target-dir",
+ "target/remote_server",
+ "--target",
+ &triple,
+ ])
+ .env("RUSTFLAGS", &rust_flags),
+ )
+ .await?;
+ } else if build_remote_server.contains("cross") {
+ #[cfg(target_os = "windows")]
+ use util::paths::SanitizedPath;
+
+ delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
+ log::info!("installing cross");
+ run_cmd(Command::new("cargo").args([
+ "install",
+ "cross",
+ "--git",
+ "https://github.com/cross-rs/cross",
+ ]))
+ .await?;
+
+ delegate.set_status(
+ Some(&format!(
+ "Building remote server binary from source for {} with Docker",
+ &triple
+ )),
+ cx,
+ );
+ log::info!("building remote server binary from source for {}", &triple);
+
+ // On Windows, the binding needs to be set to the canonical path
+ #[cfg(target_os = "windows")]
+ let src =
+ SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string();
+ #[cfg(not(target_os = "windows"))]
+ let src = "./target";
+ run_cmd(
+ Command::new("cross")
+ .args([
+ "build",
+ "--package",
+ "remote_server",
+ "--features",
+ "debug-embed",
+ "--target-dir",
+ "target/remote_server",
+ "--target",
+ &triple,
+ ])
+ .env(
+ "CROSS_CONTAINER_OPTS",
+ format!("--mount type=bind,src={src},dst=/app/target"),
+ )
+ .env("RUSTFLAGS", &rust_flags),
+ )
+ .await?;
+ } else {
+ let which = cx
+ .background_spawn(async move { which::which("zig") })
+ .await;
+
+ if which.is_err() {
+ #[cfg(not(target_os = "windows"))]
+ {
+ anyhow::bail!(
+ "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
+ )
+ }
+ #[cfg(target_os = "windows")]
+ {
+ anyhow::bail!(
+ "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
+ )
+ }
+ }
+
+ delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
+ log::info!("adding rustup target");
+ run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
+
+ delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
+ log::info!("installing cargo-zigbuild");
+ run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
+
+ delegate.set_status(
+ Some(&format!(
+ "Building remote binary from source for {triple} with Zig"
+ )),
+ cx,
+ );
+ log::info!("building remote binary from source for {triple} with Zig");
+ run_cmd(
+ Command::new("cargo")
+ .args([
+ "zigbuild",
+ "--package",
+ "remote_server",
+ "--features",
+ "debug-embed",
+ "--target-dir",
+ "target/remote_server",
+ "--target",
+ &triple,
+ ])
+ .env("RUSTFLAGS", &rust_flags),
+ )
+ .await?;
+ };
+ let bin_path = Path::new("target")
+ .join("remote_server")
+ .join(&triple)
+ .join("debug")
+ .join("remote_server");
+
+ let path = if !build_remote_server.contains("nocompress") {
+ delegate.set_status(Some("Compressing binary"), cx);
+
+ #[cfg(not(target_os = "windows"))]
+ {
+ run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
+ }
+ #[cfg(target_os = "windows")]
+ {
+ // On Windows, we use 7z to compress the binary
+ let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
+ let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
+ if smol::fs::metadata(&gz_path).await.is_ok() {
+ smol::fs::remove_file(&gz_path).await?;
+ }
+ run_cmd(Command::new(seven_zip).args([
+ "a",
+ "-tgzip",
+ &gz_path,
+ &bin_path.to_string_lossy(),
+ ]))
+ .await?;
+ }
+
+ let mut archive_path = bin_path;
+ archive_path.set_extension("gz");
+ std::env::current_dir()?.join(archive_path)
+ } else {
+ bin_path
+ };
+
+ Ok(path)
+ }
+}
+
+impl SshSocket {
+ #[cfg(not(target_os = "windows"))]
+ fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result<Self> {
+ Ok(Self {
+ connection_options: options,
+ envs: HashMap::default(),
+ socket_path,
+ })
+ }
+
+ #[cfg(target_os = "windows")]
+ fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result<Self> {
+ let askpass_script = temp_dir.path().join("askpass.bat");
+ std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?;
+ let mut envs = HashMap::default();
+ envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into());
+ envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string());
+ envs.insert("ZED_SSH_ASKPASS".into(), secret);
+ Ok(Self {
+ connection_options: options,
+ envs,
+ })
+ }
+
+ // :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
+ // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
+ // and passes -l as an argument to sh, not to ls.
+ // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing
+ // into a machine. You must use `cd` to get back to $HOME.
+ // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'"
+ fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command {
+ let mut command = util::command::new_smol_command("ssh");
+ let to_run = iter::once(&program)
+ .chain(args.iter())
+ .map(|token| {
+ // We're trying to work with: sh, bash, zsh, fish, tcsh, ...?
+ debug_assert!(
+ !token.contains('\n'),
+ "multiline arguments do not work in all shells"
+ );
+ shlex::try_quote(token).unwrap()
+ })
+ .join(" ");
+ let to_run = format!("cd; {to_run}");
+ log::debug!("ssh {} {:?}", self.connection_options.ssh_url(), to_run);
+ self.ssh_options(&mut command)
+ .arg(self.connection_options.ssh_url())
+ .arg(to_run);
+ command
+ }
+
+ async fn run_command(&self, program: &str, args: &[&str]) -> Result<String> {
+ let output = self.ssh_command(program, args).output().await?;
+ anyhow::ensure!(
+ output.status.success(),
+ "failed to run command: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
+ command
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .args(self.connection_options.additional_args())
+ .args(["-o", "ControlMaster=no", "-o"])
+ .arg(format!("ControlPath={}", self.socket_path.display()))
+ }
+
+ #[cfg(target_os = "windows")]
+ fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
+ command
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .args(self.connection_options.additional_args())
+ .envs(self.envs.clone())
+ }
+
+ // 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
+ #[cfg(not(target_os = "windows"))]
+ fn ssh_args(&self) -> Vec<String> {
+ let mut arguments = self.connection_options.additional_args();
+ arguments.extend(vec![
+ "-o".to_string(),
+ "ControlMaster=no".to_string(),
+ "-o".to_string(),
+ format!("ControlPath={}", self.socket_path.display()),
+ self.connection_options.ssh_url(),
+ ]);
+ arguments
+ }
+
+ #[cfg(target_os = "windows")]
+ fn ssh_args(&self) -> Vec<String> {
+ let mut arguments = self.connection_options.additional_args();
+ arguments.push(self.connection_options.ssh_url());
+ arguments
+ }
+
+ async fn platform(&self) -> Result<RemotePlatform> {
+ let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?;
+ let Some((os, arch)) = uname.split_once(" ") else {
+ anyhow::bail!("unknown uname: {uname:?}")
+ };
+
+ let os = match os.trim() {
+ "Darwin" => "macos",
+ "Linux" => "linux",
+ _ => anyhow::bail!(
+ "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
+ ),
+ };
+ // exclude armv5,6,7 as they are 32-bit.
+ let arch = if arch.starts_with("armv8")
+ || arch.starts_with("armv9")
+ || arch.starts_with("arm64")
+ || arch.starts_with("aarch64")
+ {
+ "aarch64"
+ } else if arch.starts_with("x86") {
+ "x86_64"
+ } else {
+ anyhow::bail!(
+ "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
+ )
+ };
+
+ Ok(RemotePlatform { os, arch })
+ }
+
+ async fn shell(&self) -> String {
+ match self.run_command("sh", &["-lc", "echo $SHELL"]).await {
+ Ok(shell) => shell.trim().to_owned(),
+ Err(e) => {
+ log::error!("Failed to get shell: {e}");
+ "sh".to_owned()
+ }
+ }
+ }
+}
+
+fn parse_port_number(port_str: &str) -> Result<u16> {
+ port_str
+ .parse()
+ .with_context(|| format!("parsing port number: {port_str}"))
+}
+
+fn parse_port_forward_spec(spec: &str) -> Result<SshPortForwardOption> {
+ let parts: Vec<&str> = spec.split(':').collect();
+
+ match parts.len() {
+ 4 => {
+ let local_port = parse_port_number(parts[1])?;
+ let remote_port = parse_port_number(parts[3])?;
+
+ Ok(SshPortForwardOption {
+ local_host: Some(parts[0].to_string()),
+ local_port,
+ remote_host: Some(parts[2].to_string()),
+ remote_port,
+ })
+ }
+ 3 => {
+ let local_port = parse_port_number(parts[0])?;
+ let remote_port = parse_port_number(parts[2])?;
+
+ Ok(SshPortForwardOption {
+ local_host: None,
+ local_port,
+ remote_host: Some(parts[1].to_string()),
+ remote_port,
+ })
+ }
+ _ => anyhow::bail!("Invalid port forward format"),
+ }
+}
+
+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();
+ let mut port_forwards: Vec<SshPortForwardOption> = 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", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R",
+ "-w",
+ ];
+
+ let mut tokens = shlex::split(input).context("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;
+ }
+ if arg == "-L" || arg.starts_with("-L") {
+ let forward_spec = if arg == "-L" {
+ tokens.next()
+ } else {
+ Some(arg.strip_prefix("-L").unwrap().to_string())
+ };
+
+ if let Some(spec) = forward_spec {
+ port_forwards.push(parse_port_forward_spec(&spec)?);
+ } else {
+ anyhow::bail!("Missing port forward format");
+ }
+ }
+
+ 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;
+ // Destination might be: username1@username2@ip2@ip1
+ if let Some((u, rest)) = input.rsplit_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");
+ };
+
+ let port_forwards = match port_forwards.len() {
+ 0 => None,
+ _ => Some(port_forwards),
+ };
+
+ Ok(Self {
+ host: hostname,
+ username,
+ port,
+ port_forwards,
+ args: Some(args),
+ password: None,
+ nickname: None,
+ upload_binary_over_ssh: false,
+ })
+ }
+
+ pub fn ssh_url(&self) -> String {
+ let mut result = String::from("ssh://");
+ if let Some(username) = &self.username {
+ // Username might be: username1@username2@ip2
+ let username = urlencoding::encode(username);
+ result.push_str(&username);
+ result.push('@');
+ }
+ result.push_str(&self.host);
+ if let Some(port) = self.port {
+ result.push(':');
+ result.push_str(&port.to_string());
+ }
+ result
+ }
+
+ pub fn additional_args(&self) -> Vec<String> {
+ let mut args = self.args.iter().flatten().cloned().collect::<Vec<String>>();
+
+ if let Some(forwards) = &self.port_forwards {
+ args.extend(forwards.iter().map(|pf| {
+ let local_host = match &pf.local_host {
+ Some(host) => host,
+ None => "localhost",
+ };
+ let remote_host = match &pf.remote_host {
+ Some(host) => host,
+ None => "localhost",
+ };
+
+ format!(
+ "-L{}:{}:{}:{}",
+ local_host, pf.local_port, remote_host, pf.remote_port
+ )
+ }));
+ }
+
+ args
+ }
+
+ fn scp_url(&self) -> String {
+ if let Some(username) = &self.username {
+ format!("{}@{}", username, self.host)
+ } else {
+ self.host.clone()
+ }
+ }
+
+ pub fn connection_string(&self) -> String {
+ let host = if let Some(username) = &self.username {
+ format!("{}@{}", username, self.host)
+ } else {
+ self.host.clone()
+ };
+ if let Some(port) = &self.port {
+ format!("{}:{}", host, port)
+ } else {
+ host
+ }
+ }
+}
@@ -21,7 +21,7 @@ use project::{
};
use rpc::{
AnyProtoClient, TypedEnvelope,
- proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
+ proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID},
};
use settings::initial_server_settings_content;
@@ -83,7 +83,7 @@ impl HeadlessProject {
let worktree_store = cx.new(|cx| {
let mut store = WorktreeStore::local(true, fs.clone());
- store.shared(SSH_PROJECT_ID, session.clone(), cx);
+ store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
store
});
@@ -101,7 +101,7 @@ impl HeadlessProject {
let buffer_store = cx.new(|cx| {
let mut buffer_store = BufferStore::local(worktree_store.clone(), cx);
- buffer_store.shared(SSH_PROJECT_ID, session.clone(), cx);
+ buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
buffer_store
});
@@ -119,7 +119,7 @@ impl HeadlessProject {
breakpoint_store.clone(),
cx,
);
- dap_store.shared(SSH_PROJECT_ID, session.clone(), cx);
+ dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
dap_store
});
@@ -131,7 +131,7 @@ impl HeadlessProject {
fs.clone(),
cx,
);
- store.shared(SSH_PROJECT_ID, session.clone(), cx);
+ store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
store
});
@@ -154,7 +154,7 @@ impl HeadlessProject {
environment.clone(),
cx,
);
- task_store.shared(SSH_PROJECT_ID, session.clone(), cx);
+ task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
task_store
});
let settings_observer = cx.new(|cx| {
@@ -164,7 +164,7 @@ impl HeadlessProject {
task_store.clone(),
cx,
);
- observer.shared(SSH_PROJECT_ID, session.clone(), cx);
+ observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
observer
});
@@ -185,7 +185,7 @@ impl HeadlessProject {
fs.clone(),
cx,
);
- lsp_store.shared(SSH_PROJECT_ID, session.clone(), cx);
+ lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
lsp_store
});
@@ -213,15 +213,15 @@ impl HeadlessProject {
);
// local_machine -> ssh handlers
- session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
- session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
- session.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity());
- session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store);
- session.subscribe_to_entity(SSH_PROJECT_ID, &task_store);
- session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store);
- session.subscribe_to_entity(SSH_PROJECT_ID, &dap_store);
- session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
- session.subscribe_to_entity(SSH_PROJECT_ID, &git_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer);
+ session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store);
session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory);
session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata);
@@ -288,7 +288,7 @@ impl HeadlessProject {
} = event
{
cx.background_spawn(self.session.request(proto::UpdateBuffer {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
buffer_id: buffer.read(cx).remote_id().to_proto(),
operations: vec![serialize_operation(operation)],
}))
@@ -310,7 +310,7 @@ impl HeadlessProject {
} => {
self.session
.send(proto::UpdateLanguageServer {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
server_name: name.as_ref().map(|name| name.to_string()),
language_server_id: language_server_id.to_proto(),
variant: Some(message.clone()),
@@ -320,7 +320,7 @@ impl HeadlessProject {
LspStoreEvent::Notification(message) => {
self.session
.send(proto::Toast {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
notification_id: "lsp".to_string(),
message: message.clone(),
})
@@ -329,7 +329,7 @@ impl HeadlessProject {
LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
self.session
.send(proto::LanguageServerLog {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
language_server_id: language_server_id.to_proto(),
message: message.clone(),
log_type: Some(log_type.to_proto()),
@@ -338,7 +338,7 @@ impl HeadlessProject {
}
LspStoreEvent::LanguageServerPrompt(prompt) => {
let request = self.session.request(proto::LanguageServerPromptRequest {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
actions: prompt
.actions
.iter()
@@ -474,7 +474,7 @@ impl HeadlessProject {
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
buffer_store.update(&mut cx, |buffer_store, cx| {
buffer_store
- .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
+ .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
.detach_and_log_err(cx);
})?;
@@ -500,7 +500,7 @@ impl HeadlessProject {
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
buffer_store.update(&mut cx, |buffer_store, cx| {
buffer_store
- .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
+ .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
.detach_and_log_err(cx);
})?;
@@ -550,7 +550,7 @@ impl HeadlessProject {
buffer_store.update(cx, |buffer_store, cx| {
buffer_store
- .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
+ .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
.detach_and_log_err(cx);
});
@@ -586,7 +586,7 @@ impl HeadlessProject {
response.buffer_ids.push(buffer_id.to_proto());
buffer_store
.update(&mut cx, |buffer_store, cx| {
- buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
+ buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
})?
.await?;
}
@@ -22,7 +22,7 @@ use project::{
Project, ProjectPath,
search::{SearchQuery, SearchResult},
};
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use serde_json::json;
use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
use smol::stream::StreamExt;
@@ -1119,7 +1119,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext)
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
- let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
+ let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
client
.update(cx, |client, cx| client.simulate_disconnect(cx))
.detach();
@@ -1782,7 +1782,7 @@ pub async fn init_test(
});
init_logger();
- let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
+ let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
@@ -1804,7 +1804,7 @@ pub async fn init_test(
)
});
- let ssh = SshRemoteClient::fake_client(opts, cx).await;
+ let ssh = RemoteClient::fake_client(opts, cx).await;
let project = build_project(ssh, cx);
project
.update(cx, {
@@ -1819,7 +1819,7 @@ fn init_logger() {
zlog::init_test();
}
-fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
+fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
cx.update(|cx| {
if !cx.has_global::<SettingsStore>() {
let settings_store = SettingsStore::test(cx);
@@ -1845,5 +1845,5 @@ fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entit
language::init(cx);
});
- cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
+ cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
}
@@ -19,14 +19,14 @@ use project::project_settings::ProjectSettings;
use proto::CrashReport;
use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel};
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use remote::{
json_log::LogRecord,
protocol::{read_message, write_message},
proxy::ProxyLaunchError,
};
use reqwest_client::ReqwestClient;
-use rpc::proto::{self, Envelope, SSH_PROJECT_ID};
+use rpc::proto::{self, Envelope, REMOTE_SERVER_PROJECT_ID};
use rpc::{AnyProtoClient, TypedEnvelope};
use settings::{Settings, SettingsStore, watch_config_file};
use smol::channel::{Receiver, Sender};
@@ -396,7 +396,7 @@ fn start_server(
})
.detach();
- SshRemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server")
+ RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server")
}
fn init_paths() -> anyhow::Result<()> {
@@ -867,34 +867,21 @@ where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
- use remote::protocol::read_message_raw;
+ use remote::protocol::{read_message_raw, write_size_prefixed_buffer};
let mut buffer = Vec::new();
loop {
read_message_raw(&mut reader, &mut buffer)
.await
.with_context(|| format!("failed to read message from {}", socket_name))?;
-
write_size_prefixed_buffer(&mut writer, &mut buffer)
.await
.with_context(|| format!("failed to write message to {}", socket_name))?;
-
writer.flush().await?;
-
buffer.clear();
}
}
-async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
- stream: &mut S,
- buffer: &mut Vec<u8>,
-) -> Result<()> {
- let len = buffer.len() as u32;
- stream.write_all(len.to_le_bytes().as_slice()).await?;
- stream.write_all(buffer).await?;
- Ok(())
-}
-
fn initialize_settings(
session: AnyProtoClient,
fs: Arc<dyn Fs>,
@@ -910,7 +897,7 @@ fn initialize_settings(
session
.send(proto::Toast {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
notification_id: "server-settings-failed".to_string(),
message: format!(
"Error in settings on remote host {:?}: {}",
@@ -922,7 +909,7 @@ fn initialize_settings(
} else {
session
.send(proto::HideToast {
- project_id: SSH_PROJECT_ID,
+ project_id: REMOTE_SERVER_PROJECT_ID,
notification_id: "server-settings-failed".to_string(),
})
.log_err();
@@ -36,7 +36,7 @@ impl ShellKind {
} else if program == "csh" {
ShellKind::Csh
} else {
- // Someother shell detected, the user might install and use a
+ // Some other shell detected, the user might install and use a
// unix-like shell.
ShellKind::Posix
}
@@ -203,14 +203,15 @@ pub struct ShellBuilder {
impl ShellBuilder {
/// Create a new ShellBuilder as configured.
pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self {
- let (program, args) = match shell {
- Shell::System => match remote_system_shell {
- Some(remote_shell) => (remote_shell.to_string(), Vec::new()),
- None => (system_shell(), Vec::new()),
+ let (program, args) = match remote_system_shell {
+ Some(program) => (program.to_string(), Vec::new()),
+ None => match shell {
+ Shell::System => (system_shell(), Vec::new()),
+ Shell::Program(shell) => (shell.clone(), Vec::new()),
+ Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
},
- Shell::Program(shell) => (shell.clone(), Vec::new()),
- Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
};
+
let kind = ShellKind::new(&program);
Self {
program,
@@ -1403,7 +1403,7 @@ impl InputHandler for TerminalInputHandler {
window.invalidate_character_coordinates();
let project = this.project().read(cx);
let telemetry = project.client().telemetry().clone();
- telemetry.log_edit_event("terminal", project.is_via_ssh());
+ telemetry.log_edit_event("terminal", project.is_via_remote_server());
})
.ok();
}
@@ -481,17 +481,28 @@ impl TerminalPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<WeakEntity<Terminal>>> {
- let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| {
- let project = workspace.project().read(cx);
- (
- project.ssh_client().and_then(|it| it.read(cx).ssh_info()),
- project.is_via_collab(),
- )
- }) else {
- return Task::ready(Err(anyhow!("Project is not local")));
+ let remote_client = self
+ .workspace
+ .update(cx, |workspace, cx| {
+ let project = workspace.project().read(cx);
+ if project.is_via_collab() {
+ Err(anyhow!("cannot spawn tasks as a guest"))
+ } else {
+ Ok(project.remote_client())
+ }
+ })
+ .flatten();
+
+ let remote_client = match remote_client {
+ Ok(remote_client) => remote_client,
+ Err(e) => return Task::ready(Err(e)),
};
- let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell);
+ let remote_shell = remote_client
+ .as_ref()
+ .and_then(|remote_client| remote_client.read(cx).shell());
+
+ let builder = ShellBuilder::new(remote_shell.as_deref(), &task.shell);
let command_label = builder.command_label(&task.command_label);
let (command, args) = builder.build(task.command.clone(), &task.args);
@@ -337,7 +337,7 @@ impl TitleBar {
let room = room.read(cx);
let project = self.project.read(cx);
- let is_local = project.is_local() || project.is_via_ssh();
+ let is_local = project.is_local() || project.is_via_remote_server();
let is_shared = is_local && project.is_shared();
let is_muted = room.is_muted();
let muted_by_user = room.muted_by_user();
@@ -299,8 +299,8 @@ impl TitleBar {
}
}
- fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
- let options = self.project.read(cx).ssh_connection_options(cx)?;
+ fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
+ let options = self.project.read(cx).remote_connection_options(cx)?;
let host: SharedString = options.connection_string().into();
let nickname = options
@@ -308,7 +308,7 @@ impl TitleBar {
.map(|nick| nick.into())
.unwrap_or_else(|| host.clone());
- let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
+ let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
remote::ConnectionState::HeartbeatMissed => (
@@ -324,7 +324,7 @@ impl TitleBar {
}
};
- let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
+ let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
remote::ConnectionState::Connecting => Color::Info,
remote::ConnectionState::Connected => Color::Default,
remote::ConnectionState::HeartbeatMissed => Color::Warning,
@@ -379,8 +379,8 @@ impl TitleBar {
}
pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
- if self.project.read(cx).is_via_ssh() {
- return self.render_ssh_project_host(cx);
+ if self.project.read(cx).is_via_remote_server() {
+ return self.render_remote_project_connection(cx);
}
if self.project.read(cx).is_disconnected(cx) {
@@ -1924,7 +1924,9 @@ impl ShellExec {
let Some(range) = input_range else { return };
- let mut process = project.read(cx).exec_in_shell(command, cx);
+ let Some(mut process) = project.read(cx).exec_in_shell(command, cx).log_err() else {
+ return;
+ };
process.stdout(Stdio::piped());
process.stderr(Stdio::piped());
@@ -20,7 +20,7 @@ impl Workspace {
window: &mut Window,
cx: &mut Context<Self>,
) {
- match self.project.read(cx).ssh_connection_state(cx) {
+ match self.project.read(cx).remote_connection_state(cx) {
None | Some(ConnectionState::Connected) => {}
Some(
ConnectionState::Connecting
@@ -74,7 +74,7 @@ use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
};
-use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
+use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier};
use schemars::JsonSchema;
use serde::Deserialize;
use session::AppSession;
@@ -2084,7 +2084,7 @@ impl Workspace {
cx: &mut Context<Self>,
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
if self.project.read(cx).is_via_collab()
- || self.project.read(cx).is_via_ssh()
+ || self.project.read(cx).is_via_remote_server()
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
{
let prompt = self.on_prompt_for_new_path.take().unwrap();
@@ -5249,7 +5249,7 @@ impl Workspace {
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
let paths = PathList::new(&self.root_paths(cx));
- if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) {
+ if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
WorkspaceLocation::Location(
SerializedWorkspaceLocation::Ssh(SerializedSshConnection {
host: connection.host,
@@ -6917,7 +6917,7 @@ async fn join_channel_internal(
return None;
}
- if (project.is_local() || project.is_via_ssh())
+ if (project.is_local() || project.is_via_remote_server())
&& project.visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()
@@ -7263,7 +7263,7 @@ pub fn open_ssh_project_with_new_connection(
window: WindowHandle<Workspace>,
connection_options: SshConnectionOptions,
cancel_rx: oneshot::Receiver<()>,
- delegate: Arc<dyn SshClientDelegate>,
+ delegate: Arc<dyn RemoteClientDelegate>,
app_state: Arc<AppState>,
paths: Vec<PathBuf>,
cx: &mut App,
@@ -7274,7 +7274,7 @@ pub fn open_ssh_project_with_new_connection(
let session = match cx
.update(|cx| {
- remote::SshRemoteClient::new(
+ remote::RemoteClient::ssh(
ConnectionIdentifier::Workspace(workspace_id.0),
connection_options,
cancel_rx,
@@ -7289,7 +7289,7 @@ pub fn open_ssh_project_with_new_connection(
};
let project = cx.update(|cx| {
- project::Project::ssh(
+ project::Project::remote(
session,
app_state.client.clone(),
app_state.node_runtime.clone(),
@@ -220,10 +220,10 @@ pub fn init(
let installation_id = installation_id.clone();
let system_id = system_id.clone();
- let Some(ssh_client) = project.ssh_client() else {
+ let Some(remote_client) = project.remote_client() else {
return;
};
- ssh_client.update(cx, |client, cx| {
+ remote_client.update(cx, |client, cx| {
if !TelemetrySettings::get_global(cx).diagnostics {
return;
}
@@ -918,7 +918,7 @@ fn register_actions(
capture_audio(workspace, window, cx);
});
- if workspace.project().read(cx).is_via_ssh() {
+ if workspace.project().read(cx).is_via_remote_server() {
workspace.register_action({
move |workspace, _: &OpenServerSettings, window, cx| {
let open_server_settings = workspace
@@ -1543,7 +1543,7 @@ pub fn open_new_ssh_project_from_project(
cx: &mut Context<Workspace>,
) -> Task<anyhow::Result<()>> {
let app_state = workspace.app_state().clone();
- let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
+ let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
};
let connection_options = ssh_client.read(cx).connection_options();