ssh remoting: Add infrastructure to handle reconnects (#18572)
This restructures the code in `remote` so that it's easier to replace the current SSH connection with a new one in case of disconnects/reconnects. Right now, it successfully reconnects, BUT we're still missing the big piece on the server-side: keeping the server process alive and reconnecting to the same process that keeps the project-state. Release Notes: - N/A --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
527c9097f8
commit
7ce8797d78
11 changed files with 562 additions and 401 deletions
|
@ -4,7 +4,7 @@ use fs::{FakeFs, Fs as _};
|
||||||
use gpui::{Context as _, TestAppContext};
|
use gpui::{Context as _, TestAppContext};
|
||||||
use language::language_settings::all_language_settings;
|
use language::language_settings::all_language_settings;
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
use remote::SshSession;
|
use remote::SshRemoteClient;
|
||||||
use remote_server::HeadlessProject;
|
use remote_server::HeadlessProject;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
@ -24,7 +24,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Set up project on remote FS
|
// Set up project on remote FS
|
||||||
let (client_ssh, server_ssh) = SshSession::fake(cx_a, server_cx);
|
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx);
|
||||||
let remote_fs = FakeFs::new(server_cx.executor());
|
let remote_fs = FakeFs::new(server_cx.executor());
|
||||||
remote_fs
|
remote_fs
|
||||||
.insert_tree(
|
.insert_tree(
|
||||||
|
|
|
@ -25,7 +25,7 @@ use node_runtime::NodeRuntime;
|
||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
use remote::SshSession;
|
use remote::SshRemoteClient;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelRole},
|
proto::{self, ChannelRole},
|
||||||
RECEIVE_TIMEOUT,
|
RECEIVE_TIMEOUT,
|
||||||
|
@ -835,7 +835,7 @@ impl TestClient {
|
||||||
pub async fn build_ssh_project(
|
pub async fn build_ssh_project(
|
||||||
&self,
|
&self,
|
||||||
root_path: impl AsRef<Path>,
|
root_path: impl AsRef<Path>,
|
||||||
ssh: Arc<SshSession>,
|
ssh: Arc<SshRemoteClient>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (Model<Project>, WorktreeId) {
|
) -> (Model<Project>, WorktreeId) {
|
||||||
let project = cx.update(|cx| {
|
let project = cx.update(|cx| {
|
||||||
|
|
|
@ -54,7 +54,7 @@ use parking_lot::{Mutex, RwLock};
|
||||||
use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
|
use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
|
||||||
pub use prettier_store::PrettierStore;
|
pub use prettier_store::PrettierStore;
|
||||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||||
use remote::SshSession;
|
use remote::SshRemoteClient;
|
||||||
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
|
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
|
||||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||||
use search_history::SearchHistory;
|
use search_history::SearchHistory;
|
||||||
|
@ -138,7 +138,7 @@ pub struct Project {
|
||||||
join_project_response_message_id: u32,
|
join_project_response_message_id: u32,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
ssh_session: Option<Arc<SshSession>>,
|
ssh_client: Option<Arc<SshRemoteClient>>,
|
||||||
client_state: ProjectClientState,
|
client_state: ProjectClientState,
|
||||||
collaborators: HashMap<proto::PeerId, Collaborator>,
|
collaborators: HashMap<proto::PeerId, Collaborator>,
|
||||||
client_subscriptions: Vec<client::Subscription>,
|
client_subscriptions: Vec<client::Subscription>,
|
||||||
|
@ -643,7 +643,7 @@ impl Project {
|
||||||
user_store,
|
user_store,
|
||||||
settings_observer,
|
settings_observer,
|
||||||
fs,
|
fs,
|
||||||
ssh_session: None,
|
ssh_client: None,
|
||||||
buffers_needing_diff: Default::default(),
|
buffers_needing_diff: Default::default(),
|
||||||
git_diff_debouncer: DebouncedDelay::new(),
|
git_diff_debouncer: DebouncedDelay::new(),
|
||||||
terminals: Terminals {
|
terminals: Terminals {
|
||||||
|
@ -664,7 +664,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ssh(
|
pub fn ssh(
|
||||||
ssh: Arc<SshSession>,
|
ssh: Arc<SshRemoteClient>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
node: NodeRuntime,
|
node: NodeRuntime,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
|
@ -682,14 +682,14 @@ impl Project {
|
||||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||||
|
|
||||||
let worktree_store =
|
let worktree_store =
|
||||||
cx.new_model(|_| WorktreeStore::remote(false, ssh.clone().into(), 0, None));
|
cx.new_model(|_| WorktreeStore::remote(false, ssh.to_proto_client(), 0, None));
|
||||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let buffer_store = cx.new_model(|cx| {
|
let buffer_store = cx.new_model(|cx| {
|
||||||
BufferStore::remote(
|
BufferStore::remote(
|
||||||
worktree_store.clone(),
|
worktree_store.clone(),
|
||||||
ssh.clone().into(),
|
ssh.to_proto_client(),
|
||||||
SSH_PROJECT_ID,
|
SSH_PROJECT_ID,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -698,7 +698,7 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let settings_observer = cx.new_model(|cx| {
|
let settings_observer = cx.new_model(|cx| {
|
||||||
SettingsObserver::new_ssh(ssh.clone().into(), worktree_store.clone(), cx)
|
SettingsObserver::new_ssh(ssh.to_proto_client(), worktree_store.clone(), cx)
|
||||||
});
|
});
|
||||||
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -709,7 +709,7 @@ impl Project {
|
||||||
buffer_store.clone(),
|
buffer_store.clone(),
|
||||||
worktree_store.clone(),
|
worktree_store.clone(),
|
||||||
languages.clone(),
|
languages.clone(),
|
||||||
ssh.clone().into(),
|
ssh.to_proto_client(),
|
||||||
SSH_PROJECT_ID,
|
SSH_PROJECT_ID,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -733,7 +733,7 @@ impl Project {
|
||||||
user_store,
|
user_store,
|
||||||
settings_observer,
|
settings_observer,
|
||||||
fs,
|
fs,
|
||||||
ssh_session: Some(ssh.clone()),
|
ssh_client: Some(ssh.clone()),
|
||||||
buffers_needing_diff: Default::default(),
|
buffers_needing_diff: Default::default(),
|
||||||
git_diff_debouncer: DebouncedDelay::new(),
|
git_diff_debouncer: DebouncedDelay::new(),
|
||||||
terminals: Terminals {
|
terminals: Terminals {
|
||||||
|
@ -751,7 +751,7 @@ impl Project {
|
||||||
search_excluded_history: Self::new_search_history(),
|
search_excluded_history: Self::new_search_history(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let client: AnyProtoClient = ssh.clone().into();
|
let client: AnyProtoClient = ssh.to_proto_client();
|
||||||
|
|
||||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
|
ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
|
||||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
|
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
|
||||||
|
@ -907,7 +907,7 @@ impl Project {
|
||||||
user_store: user_store.clone(),
|
user_store: user_store.clone(),
|
||||||
snippets,
|
snippets,
|
||||||
fs,
|
fs,
|
||||||
ssh_session: None,
|
ssh_client: None,
|
||||||
settings_observer: settings_observer.clone(),
|
settings_observer: settings_observer.clone(),
|
||||||
client_subscriptions: Default::default(),
|
client_subscriptions: Default::default(),
|
||||||
_subscriptions: vec![cx.on_release(Self::release)],
|
_subscriptions: vec![cx.on_release(Self::release)],
|
||||||
|
@ -1230,7 +1230,7 @@ impl Project {
|
||||||
match self.client_state {
|
match self.client_state {
|
||||||
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
||||||
_ => {
|
_ => {
|
||||||
if self.ssh_session.is_some() {
|
if self.ssh_client.is_some() {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
|
@ -1638,7 +1638,7 @@ impl Project {
|
||||||
pub fn is_local(&self) -> bool {
|
pub fn is_local(&self) -> bool {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||||
self.ssh_session.is_none()
|
self.ssh_client.is_none()
|
||||||
}
|
}
|
||||||
ProjectClientState::Remote { .. } => false,
|
ProjectClientState::Remote { .. } => false,
|
||||||
}
|
}
|
||||||
|
@ -1647,7 +1647,7 @@ impl Project {
|
||||||
pub fn is_via_ssh(&self) -> bool {
|
pub fn is_via_ssh(&self) -> bool {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||||
self.ssh_session.is_some()
|
self.ssh_client.is_some()
|
||||||
}
|
}
|
||||||
ProjectClientState::Remote { .. } => false,
|
ProjectClientState::Remote { .. } => false,
|
||||||
}
|
}
|
||||||
|
@ -1933,8 +1933,9 @@ impl Project {
|
||||||
}
|
}
|
||||||
BufferStoreEvent::BufferChangedFilePath { .. } => {}
|
BufferStoreEvent::BufferChangedFilePath { .. } => {}
|
||||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||||
if let Some(ref ssh_session) = self.ssh_session {
|
if let Some(ref ssh_client) = self.ssh_client {
|
||||||
ssh_session
|
ssh_client
|
||||||
|
.to_proto_client()
|
||||||
.send(proto::CloseBuffer {
|
.send(proto::CloseBuffer {
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
buffer_id: buffer_id.to_proto(),
|
buffer_id: buffer_id.to_proto(),
|
||||||
|
@ -2139,13 +2140,14 @@ impl Project {
|
||||||
} => {
|
} => {
|
||||||
let operation = language::proto::serialize_operation(operation);
|
let operation = language::proto::serialize_operation(operation);
|
||||||
|
|
||||||
if let Some(ssh) = &self.ssh_session {
|
if let Some(ssh) = &self.ssh_client {
|
||||||
ssh.send(proto::UpdateBuffer {
|
ssh.to_proto_client()
|
||||||
project_id: 0,
|
.send(proto::UpdateBuffer {
|
||||||
buffer_id: buffer_id.to_proto(),
|
project_id: 0,
|
||||||
operations: vec![operation.clone()],
|
buffer_id: buffer_id.to_proto(),
|
||||||
})
|
operations: vec![operation.clone()],
|
||||||
.ok();
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
|
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
|
||||||
|
@ -2825,14 +2827,13 @@ impl Project {
|
||||||
) -> Receiver<Model<Buffer>> {
|
) -> Receiver<Model<Buffer>> {
|
||||||
let (tx, rx) = smol::channel::unbounded();
|
let (tx, rx) = smol::channel::unbounded();
|
||||||
|
|
||||||
let (client, remote_id): (AnyProtoClient, _) =
|
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
|
||||||
if let Some(ssh_session) = self.ssh_session.clone() {
|
(ssh_client.to_proto_client(), 0)
|
||||||
(ssh_session.into(), 0)
|
} else if let Some(remote_id) = self.remote_id() {
|
||||||
} else if let Some(remote_id) = self.remote_id() {
|
(self.client.clone().into(), remote_id)
|
||||||
(self.client.clone().into(), remote_id)
|
} else {
|
||||||
} else {
|
return rx;
|
||||||
return rx;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let request = client.request(proto::FindSearchCandidates {
|
let request = client.request(proto::FindSearchCandidates {
|
||||||
project_id: remote_id,
|
project_id: remote_id,
|
||||||
|
@ -2961,11 +2962,13 @@ impl Project {
|
||||||
|
|
||||||
exists.then(|| ResolvedPath::AbsPath(expanded))
|
exists.then(|| ResolvedPath::AbsPath(expanded))
|
||||||
})
|
})
|
||||||
} else if let Some(ssh_session) = self.ssh_session.as_ref() {
|
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||||
let request = ssh_session.request(proto::CheckFileExists {
|
let request = ssh_client
|
||||||
project_id: SSH_PROJECT_ID,
|
.to_proto_client()
|
||||||
path: path.to_string(),
|
.request(proto::CheckFileExists {
|
||||||
});
|
project_id: SSH_PROJECT_ID,
|
||||||
|
path: path.to_string(),
|
||||||
|
});
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let response = request.await.log_err()?;
|
let response = request.await.log_err()?;
|
||||||
if response.exists {
|
if response.exists {
|
||||||
|
@ -3035,13 +3038,13 @@ impl Project {
|
||||||
) -> Task<Result<Vec<PathBuf>>> {
|
) -> Task<Result<Vec<PathBuf>>> {
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
|
DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
|
||||||
} else if let Some(session) = self.ssh_session.as_ref() {
|
} else if let Some(session) = self.ssh_client.as_ref() {
|
||||||
let request = proto::ListRemoteDirectory {
|
let request = proto::ListRemoteDirectory {
|
||||||
dev_server_id: SSH_PROJECT_ID,
|
dev_server_id: SSH_PROJECT_ID,
|
||||||
path: query,
|
path: query,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = session.request(request);
|
let response = session.to_proto_client().request(request);
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let response = response.await?;
|
let response = response.await?;
|
||||||
Ok(response.entries.into_iter().map(PathBuf::from).collect())
|
Ok(response.entries.into_iter().map(PathBuf::from).collect())
|
||||||
|
@ -3465,11 +3468,11 @@ impl Project {
|
||||||
cx: AsyncAppContext,
|
cx: AsyncAppContext,
|
||||||
) -> Result<proto::Ack> {
|
) -> Result<proto::Ack> {
|
||||||
let buffer_store = this.read_with(&cx, |this, cx| {
|
let buffer_store = this.read_with(&cx, |this, cx| {
|
||||||
if let Some(ssh) = &this.ssh_session {
|
if let Some(ssh) = &this.ssh_client {
|
||||||
let mut payload = envelope.payload.clone();
|
let mut payload = envelope.payload.clone();
|
||||||
payload.project_id = 0;
|
payload.project_id = 0;
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.spawn(ssh.request(payload))
|
.spawn(ssh.to_proto_client().request(payload))
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
this.buffer_store.clone()
|
this.buffer_store.clone()
|
||||||
|
|
|
@ -67,8 +67,12 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_command(&self, cx: &AppContext) -> Option<SshCommand> {
|
fn ssh_command(&self, cx: &AppContext) -> Option<SshCommand> {
|
||||||
if let Some(ssh_session) = self.ssh_session.as_ref() {
|
if let Some(args) = self
|
||||||
return Some(SshCommand::Direct(ssh_session.ssh_args()));
|
.ssh_client
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|session| session.ssh_args())
|
||||||
|
{
|
||||||
|
return Some(SshCommand::Direct(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
let dev_server_project_id = self.dev_server_project_id()?;
|
let dev_server_project_id = self.dev_server_project_id()?;
|
||||||
|
|
|
@ -11,7 +11,7 @@ use gpui::{
|
||||||
Transformation, View,
|
Transformation, View,
|
||||||
};
|
};
|
||||||
use release_channel::{AppVersion, ReleaseChannel};
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
use remote::{SshConnectionOptions, SshPlatform, SshSession};
|
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
|
@ -376,12 +376,12 @@ pub fn connect_over_ssh(
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
ui: View<SshPrompt>,
|
ui: View<SshPrompt>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<Arc<SshSession>>> {
|
) -> Task<Result<Arc<SshRemoteClient>>> {
|
||||||
let window = cx.window_handle();
|
let window = cx.window_handle();
|
||||||
let known_password = connection_options.password.clone();
|
let known_password = connection_options.password.clone();
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
remote::SshSession::client(
|
remote::SshRemoteClient::new(
|
||||||
connection_options,
|
connection_options,
|
||||||
Arc::new(SshClientDelegate {
|
Arc::new(SshClientDelegate {
|
||||||
window,
|
window,
|
||||||
|
|
|
@ -2,4 +2,4 @@ pub mod json_log;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod ssh_session;
|
pub mod ssh_session;
|
||||||
|
|
||||||
pub use ssh_session::{SshClientDelegate, SshConnectionOptions, SshPlatform, SshSession};
|
pub use ssh_session::{SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||||
|
|
|
@ -7,19 +7,23 @@ use crate::{
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{
|
||||||
|
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||||
|
oneshot,
|
||||||
|
},
|
||||||
future::BoxFuture,
|
future::BoxFuture,
|
||||||
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, StreamExt as _,
|
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
|
||||||
|
StreamExt as _,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, Task};
|
use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, Task};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
|
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
|
||||||
EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet, RpcError,
|
AnyProtoClient, EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet, RpcError,
|
||||||
};
|
};
|
||||||
use smol::{
|
use smol::{
|
||||||
fs,
|
fs,
|
||||||
process::{self, Stdio},
|
process::{self, Child, Stdio},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
|
@ -44,22 +48,6 @@ pub struct SshSocket {
|
||||||
socket_path: PathBuf,
|
socket_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SshSession {
|
|
||||||
next_message_id: AtomicU32,
|
|
||||||
response_channels: ResponseChannels, // Lock
|
|
||||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
|
||||||
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
|
|
||||||
client_socket: Option<SshSocket>,
|
|
||||||
state: Mutex<ProtoMessageHandlerSet>, // Lock
|
|
||||||
_io_task: Option<Task<Result<()>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SshClientState {
|
|
||||||
socket: SshSocket,
|
|
||||||
master_process: process::Child,
|
|
||||||
_temp_dir: TempDir,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SshConnectionOptions {
|
pub struct SshConnectionOptions {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
@ -105,18 +93,13 @@ impl SshConnectionOptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SpawnRequest {
|
|
||||||
command: String,
|
|
||||||
process_tx: oneshot::Sender<process::Child>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct SshPlatform {
|
pub struct SshPlatform {
|
||||||
pub os: &'static str,
|
pub os: &'static str,
|
||||||
pub arch: &'static str,
|
pub arch: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SshClientDelegate {
|
pub trait SshClientDelegate: Send + Sync {
|
||||||
fn ask_password(
|
fn ask_password(
|
||||||
&self,
|
&self,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
|
@ -132,48 +115,249 @@ pub trait SshClientDelegate {
|
||||||
fn set_error(&self, error_message: String, cx: &mut AsyncAppContext);
|
fn set_error(&self, error_message: String, cx: &mut AsyncAppContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
impl SshSocket {
|
||||||
|
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
||||||
|
let mut command = process::Command::new("ssh");
|
||||||
|
self.ssh_options(&mut command)
|
||||||
|
.arg(self.connection_options.ssh_url())
|
||||||
|
.arg(program);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
impl SshSession {
|
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||||
pub async fn client(
|
command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.args(["-o", "ControlMaster=no", "-o"])
|
||||||
|
.arg(format!("ControlPath={}", self.socket_path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssh_args(&self) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"-o".to_string(),
|
||||||
|
"ControlMaster=no".to_string(),
|
||||||
|
"-o".to_string(),
|
||||||
|
format!("ControlPath={}", self.socket_path.display()),
|
||||||
|
self.connection_options.ssh_url(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||||
|
let output = command.output().await?;
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"failed to run command: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn read_with_timeout(
|
||||||
|
stdout: &mut process::ChildStdout,
|
||||||
|
timeout: std::time::Duration,
|
||||||
|
output: &mut Vec<u8>,
|
||||||
|
) -> Result<(), std::io::Error> {
|
||||||
|
smol::future::or(
|
||||||
|
async {
|
||||||
|
stdout.read_to_end(output).await?;
|
||||||
|
Ok::<_, std::io::Error>(())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
smol::Timer::after(timeout).await;
|
||||||
|
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::TimedOut,
|
||||||
|
"Read operation timed out",
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChannelForwarder {
|
||||||
|
quit_tx: UnboundedSender<()>,
|
||||||
|
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelForwarder {
|
||||||
|
fn new(
|
||||||
|
mut incoming_tx: UnboundedSender<Envelope>,
|
||||||
|
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> (Self, UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
|
||||||
|
let (quit_tx, mut quit_rx) = mpsc::unbounded::<()>();
|
||||||
|
|
||||||
|
let (proxy_incoming_tx, mut proxy_incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
let (mut proxy_outgoing_tx, proxy_outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
|
||||||
|
let forwarding_task = cx.background_executor().spawn(async move {
|
||||||
|
loop {
|
||||||
|
select_biased! {
|
||||||
|
_ = quit_rx.next().fuse() => {
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
incoming_envelope = proxy_incoming_rx.next().fuse() => {
|
||||||
|
if let Some(envelope) = incoming_envelope {
|
||||||
|
if incoming_tx.send(envelope).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outgoing_envelope = outgoing_rx.next().fuse() => {
|
||||||
|
if let Some(envelope) = outgoing_envelope {
|
||||||
|
if proxy_outgoing_tx.send(envelope).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(incoming_tx, outgoing_rx)
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
forwarding_task,
|
||||||
|
quit_tx,
|
||||||
|
},
|
||||||
|
proxy_incoming_tx,
|
||||||
|
proxy_outgoing_rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_channels(mut self) -> (UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
|
||||||
|
let _ = self.quit_tx.send(()).await;
|
||||||
|
self.forwarding_task.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SshRemoteClientState {
|
||||||
|
ssh_connection: SshRemoteConnection,
|
||||||
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
|
forwarder: ChannelForwarder,
|
||||||
|
_multiplex_task: Task<Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SshRemoteClient {
|
||||||
|
client: Arc<ChannelClient>,
|
||||||
|
inner_state: Arc<Mutex<Option<SshRemoteClientState>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshRemoteClient {
|
||||||
|
pub async fn new(
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<Arc<Self>> {
|
) -> Result<Arc<Self>> {
|
||||||
let client_state = SshClientState::new(connection_options, delegate.clone(), cx).await?;
|
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
|
||||||
let platform = client_state.query_platform().await?;
|
|
||||||
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
|
||||||
let remote_binary_path = delegate.remote_server_binary_path(cx)?;
|
|
||||||
client_state
|
|
||||||
.ensure_server_binary(
|
|
||||||
&delegate,
|
|
||||||
&local_binary_path,
|
|
||||||
&remote_binary_path,
|
|
||||||
version,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let (spawn_process_tx, mut spawn_process_rx) = mpsc::unbounded::<SpawnRequest>();
|
|
||||||
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
|
|
||||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
|
||||||
let socket = client_state.socket.clone();
|
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
|
||||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
let this = Arc::new(Self {
|
||||||
|
client,
|
||||||
|
inner_state: Arc::new(Mutex::new(None)),
|
||||||
|
});
|
||||||
|
|
||||||
let mut remote_server_child = socket
|
let inner_state = {
|
||||||
.ssh_command(format!(
|
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||||
"RUST_LOG={} RUST_BACKTRACE={} {:?} run",
|
ChannelForwarder::new(incoming_tx, outgoing_rx, cx);
|
||||||
std::env::var("RUST_LOG").unwrap_or_default(),
|
|
||||||
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
|
let (ssh_connection, ssh_process) =
|
||||||
remote_binary_path,
|
Self::establish_connection(connection_options.clone(), delegate.clone(), cx)
|
||||||
))
|
.await?;
|
||||||
.spawn()
|
|
||||||
.context("failed to spawn remote server")?;
|
let multiplex_task = Self::multiplex(
|
||||||
let mut child_stderr = remote_server_child.stderr.take().unwrap();
|
this.clone(),
|
||||||
let mut child_stdout = remote_server_child.stdout.take().unwrap();
|
ssh_process,
|
||||||
let mut child_stdin = remote_server_child.stdin.take().unwrap();
|
proxy_incoming_tx,
|
||||||
|
proxy_outgoing_rx,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
SshRemoteClientState {
|
||||||
|
ssh_connection,
|
||||||
|
delegate,
|
||||||
|
forwarder: proxy,
|
||||||
|
_multiplex_task: multiplex_task,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inner_state.lock().replace(inner_state);
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconnect(this: Arc<Self>, cx: &mut AsyncAppContext) -> Result<()> {
|
||||||
|
let Some(state) = this.inner_state.lock().take() else {
|
||||||
|
return Err(anyhow!("reconnect is already in progress"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let SshRemoteClientState {
|
||||||
|
mut ssh_connection,
|
||||||
|
delegate,
|
||||||
|
forwarder: proxy,
|
||||||
|
_multiplex_task,
|
||||||
|
} = state;
|
||||||
|
drop(_multiplex_task);
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let (incoming_tx, outgoing_rx) = proxy.into_channels().await;
|
||||||
|
|
||||||
|
ssh_connection.master_process.kill()?;
|
||||||
|
ssh_connection
|
||||||
|
.master_process
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.context("Failed to kill ssh process")?;
|
||||||
|
|
||||||
|
let connection_options = ssh_connection.socket.connection_options.clone();
|
||||||
|
|
||||||
|
let (ssh_connection, ssh_process) =
|
||||||
|
Self::establish_connection(connection_options, delegate.clone(), &mut cx).await?;
|
||||||
|
|
||||||
|
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||||
|
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||||
|
|
||||||
|
let inner_state = SshRemoteClientState {
|
||||||
|
ssh_connection,
|
||||||
|
delegate,
|
||||||
|
forwarder: proxy,
|
||||||
|
_multiplex_task: Self::multiplex(
|
||||||
|
this.clone(),
|
||||||
|
ssh_process,
|
||||||
|
proxy_incoming_tx,
|
||||||
|
proxy_outgoing_rx,
|
||||||
|
&mut cx,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this.inner_state.lock().replace(inner_state);
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn multiplex(
|
||||||
|
this: Arc<Self>,
|
||||||
|
mut ssh_process: Child,
|
||||||
|
incoming_tx: UnboundedSender<Envelope>,
|
||||||
|
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let mut child_stderr = ssh_process.stderr.take().unwrap();
|
||||||
|
let mut child_stdout = ssh_process.stdout.take().unwrap();
|
||||||
|
let mut child_stdin = ssh_process.stdin.take().unwrap();
|
||||||
|
|
||||||
let io_task = cx.background_executor().spawn(async move {
|
let io_task = cx.background_executor().spawn(async move {
|
||||||
let mut stdin_buffer = Vec::new();
|
let mut stdin_buffer = Vec::new();
|
||||||
|
@ -194,27 +378,15 @@ impl SshSession {
|
||||||
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
request = spawn_process_rx.next().fuse() => {
|
|
||||||
let Some(request) = request else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!("spawn process: {:?}", request.command);
|
|
||||||
let child = client_state.socket
|
|
||||||
.ssh_command(&request.command)
|
|
||||||
.spawn()
|
|
||||||
.context("failed to create channel")?;
|
|
||||||
request.process_tx.send(child).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
result = child_stdout.read(&mut stdout_buffer).fuse() => {
|
result = child_stdout.read(&mut stdout_buffer).fuse() => {
|
||||||
match result {
|
match result {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
child_stdin.close().await?;
|
child_stdin.close().await?;
|
||||||
outgoing_rx.close();
|
outgoing_rx.close();
|
||||||
let status = remote_server_child.status().await?;
|
let status = ssh_process.status().await?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
log::error!("channel exited with status: {status:?}");
|
log::error!("ssh process exited with status: {status:?}");
|
||||||
|
return Err(anyhow!("ssh process exited with non-zero status code: {:?}", status.code()));
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -267,239 +439,112 @@ impl SshSession {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.spawn(|mut cx| async move {
|
||||||
Self::new(
|
let result = io_task.await;
|
||||||
incoming_rx,
|
|
||||||
outgoing_tx,
|
if let Err(error) = result {
|
||||||
spawn_process_tx,
|
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
|
||||||
Some(socket),
|
Self::reconnect(this, &mut cx).ok();
|
||||||
Some(io_task),
|
}
|
||||||
cx,
|
|
||||||
)
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server(
|
async fn establish_connection(
|
||||||
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
connection_options: SshConnectionOptions,
|
||||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &AppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Arc<SshSession> {
|
) -> Result<(SshRemoteConnection, Child)> {
|
||||||
let (tx, _rx) = mpsc::unbounded();
|
let ssh_connection =
|
||||||
Self::new(incoming_rx, outgoing_tx, tx, None, None, cx)
|
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
|
||||||
|
|
||||||
|
let platform = ssh_connection.query_platform().await?;
|
||||||
|
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
||||||
|
let remote_binary_path = delegate.remote_server_binary_path(cx)?;
|
||||||
|
ssh_connection
|
||||||
|
.ensure_server_binary(
|
||||||
|
&delegate,
|
||||||
|
&local_binary_path,
|
||||||
|
&remote_binary_path,
|
||||||
|
version,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let socket = ssh_connection.socket.clone();
|
||||||
|
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||||
|
|
||||||
|
let ssh_process = socket
|
||||||
|
.ssh_command(format!(
|
||||||
|
"RUST_LOG={} RUST_BACKTRACE={} {:?} run",
|
||||||
|
std::env::var("RUST_LOG").unwrap_or_default(),
|
||||||
|
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
|
||||||
|
remote_binary_path,
|
||||||
|
))
|
||||||
|
.spawn()
|
||||||
|
.context("failed to spawn remote server")?;
|
||||||
|
|
||||||
|
Ok((ssh_connection, ssh_process))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||||
|
self.client.subscribe_to_entity(remote_id, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ssh_args(&self) -> Option<Vec<String>> {
|
||||||
|
let state = self.inner_state.lock();
|
||||||
|
state
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| state.ssh_connection.socket.ssh_args())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_proto_client(&self) -> AnyProtoClient {
|
||||||
|
self.client.clone().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn fake(
|
pub fn fake(
|
||||||
client_cx: &mut gpui::TestAppContext,
|
client_cx: &mut gpui::TestAppContext,
|
||||||
server_cx: &mut gpui::TestAppContext,
|
server_cx: &mut gpui::TestAppContext,
|
||||||
) -> (Arc<Self>, Arc<Self>) {
|
) -> (Arc<Self>, Arc<ChannelClient>) {
|
||||||
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
|
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
|
||||||
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
|
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
|
||||||
let (tx, _rx) = mpsc::unbounded();
|
|
||||||
(
|
(
|
||||||
client_cx.update(|cx| {
|
client_cx.update(|cx| {
|
||||||
Self::new(
|
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
|
||||||
server_to_client_rx,
|
Arc::new(Self {
|
||||||
client_to_server_tx,
|
client,
|
||||||
tx.clone(),
|
inner_state: Arc::new(Mutex::new(None)),
|
||||||
None, // todo()
|
})
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
server_cx.update(|cx| {
|
|
||||||
Self::new(
|
|
||||||
client_to_server_rx,
|
|
||||||
server_to_client_tx,
|
|
||||||
tx.clone(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
|
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn new(
|
impl From<SshRemoteClient> for AnyProtoClient {
|
||||||
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
fn from(client: SshRemoteClient) -> Self {
|
||||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
AnyProtoClient::new(client.client.clone())
|
||||||
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
|
|
||||||
client_socket: Option<SshSocket>,
|
|
||||||
io_task: Option<Task<Result<()>>>,
|
|
||||||
cx: &AppContext,
|
|
||||||
) -> Arc<SshSession> {
|
|
||||||
let this = Arc::new(Self {
|
|
||||||
next_message_id: AtomicU32::new(0),
|
|
||||||
response_channels: ResponseChannels::default(),
|
|
||||||
outgoing_tx,
|
|
||||||
spawn_process_tx,
|
|
||||||
client_socket,
|
|
||||||
state: Default::default(),
|
|
||||||
_io_task: io_task,
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(|cx| {
|
|
||||||
let this = Arc::downgrade(&this);
|
|
||||||
async move {
|
|
||||||
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(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();
|
|
||||||
if let Some(future) = ProtoMessageHandlerSet::handle_message(
|
|
||||||
&this.state,
|
|
||||||
envelope,
|
|
||||||
this.clone().into(),
|
|
||||||
cx.clone(),
|
|
||||||
) {
|
|
||||||
log::debug!("ssh message received. name:{type_name}");
|
|
||||||
match future.await {
|
|
||||||
Ok(_) => {
|
|
||||||
log::debug!("ssh message handled. name:{type_name}");
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
log::error!(
|
|
||||||
"error handling message. type:{type_name}, error:{error}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::error!("unhandled ssh message name:{type_name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request<T: RequestMessage>(
|
|
||||||
&self,
|
|
||||||
payload: T,
|
|
||||||
) -> 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);
|
|
||||||
async move {
|
|
||||||
let response = response.await?;
|
|
||||||
log::debug!("ssh request finish. name:{}", T::NAME);
|
|
||||||
T::Response::from_envelope(response)
|
|
||||||
.ok_or_else(|| anyhow!("received a response of the wrong type"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_dynamic(
|
|
||||||
&self,
|
|
||||||
mut envelope: proto::Envelope,
|
|
||||||
type_name: &'static str,
|
|
||||||
) -> 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 = self.outgoing_tx.unbounded_send(envelope);
|
|
||||||
async move {
|
|
||||||
if let Err(error) = &result {
|
|
||||||
log::error!("failed to send message: {}", error);
|
|
||||||
return Err(anyhow!("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.outgoing_tx.unbounded_send(envelope)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
|
||||||
let id = (TypeId::of::<E>(), remote_id);
|
|
||||||
|
|
||||||
let mut state = self.state.lock();
|
|
||||||
if state.entities_by_type_and_remote_id.contains_key(&id) {
|
|
||||||
panic!("already subscribed to entity");
|
|
||||||
}
|
|
||||||
|
|
||||||
state.entities_by_type_and_remote_id.insert(
|
|
||||||
id,
|
|
||||||
EntityMessageSubscriber::Entity {
|
|
||||||
handle: entity.downgrade().into(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn spawn_process(&self, command: String) -> process::Child {
|
|
||||||
let (process_tx, process_rx) = oneshot::channel();
|
|
||||||
self.spawn_process_tx
|
|
||||||
.unbounded_send(SpawnRequest {
|
|
||||||
command,
|
|
||||||
process_tx,
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
process_rx.await.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ssh_args(&self) -> Vec<String> {
|
|
||||||
self.client_socket.as_ref().unwrap().ssh_args()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProtoClient for SshSession {
|
struct SshRemoteConnection {
|
||||||
fn request(
|
socket: SshSocket,
|
||||||
&self,
|
master_process: process::Child,
|
||||||
envelope: proto::Envelope,
|
_temp_dir: TempDir,
|
||||||
request_type: &'static str,
|
}
|
||||||
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
|
||||||
self.request_dynamic(envelope, request_type).boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
|
impl Drop for SshRemoteConnection {
|
||||||
self.send_dynamic(envelope)
|
fn drop(&mut self) {
|
||||||
}
|
if let Err(error) = self.master_process.kill() {
|
||||||
|
log::error!("failed to kill SSH master process: {}", error);
|
||||||
fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> {
|
}
|
||||||
self.send_dynamic(envelope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn message_handler_set(&self) -> &Mutex<ProtoMessageHandlerSet> {
|
|
||||||
&self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_via_collab(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshClientState {
|
impl SshRemoteConnection {
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
async fn new(
|
async fn new(
|
||||||
_connection_options: SshConnectionOptions,
|
_connection_options: SshConnectionOptions,
|
||||||
|
@ -740,74 +785,181 @@ impl SshClientState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||||
async fn read_with_timeout(
|
|
||||||
stdout: &mut process::ChildStdout,
|
|
||||||
timeout: std::time::Duration,
|
|
||||||
output: &mut Vec<u8>,
|
|
||||||
) -> Result<(), std::io::Error> {
|
|
||||||
smol::future::or(
|
|
||||||
async {
|
|
||||||
stdout.read_to_end(output).await?;
|
|
||||||
Ok::<_, std::io::Error>(())
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
smol::Timer::after(timeout).await;
|
|
||||||
|
|
||||||
Err(std::io::Error::new(
|
pub struct ChannelClient {
|
||||||
std::io::ErrorKind::TimedOut,
|
next_message_id: AtomicU32,
|
||||||
"Read operation timed out",
|
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||||
))
|
response_channels: ResponseChannels, // Lock
|
||||||
},
|
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for SshClientState {
|
impl ChannelClient {
|
||||||
fn drop(&mut self) {
|
pub fn new(
|
||||||
if let Err(error) = self.master_process.kill() {
|
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||||
log::error!("failed to kill SSH master process: {}", error);
|
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Arc<Self> {
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
outgoing_tx,
|
||||||
|
next_message_id: AtomicU32::new(0),
|
||||||
|
response_channels: ResponseChannels::default(),
|
||||||
|
message_handlers: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Self::start_handling_messages(this.clone(), incoming_rx, cx);
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_handling_messages(
|
||||||
|
this: Arc<Self>,
|
||||||
|
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) {
|
||||||
|
cx.spawn(|cx| {
|
||||||
|
let this = Arc::downgrade(&this);
|
||||||
|
async move {
|
||||||
|
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(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();
|
||||||
|
if let Some(future) = ProtoMessageHandlerSet::handle_message(
|
||||||
|
&this.message_handlers,
|
||||||
|
envelope,
|
||||||
|
this.clone().into(),
|
||||||
|
cx.clone(),
|
||||||
|
) {
|
||||||
|
log::debug!("ssh message received. name:{type_name}");
|
||||||
|
match future.await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::debug!("ssh message handled. name:{type_name}");
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!(
|
||||||
|
"error handling message. type:{type_name}, error:{error}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("unhandled ssh message name:{type_name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||||
|
let id = (TypeId::of::<E>(), remote_id);
|
||||||
|
|
||||||
|
let mut message_handlers = self.message_handlers.lock();
|
||||||
|
if message_handlers
|
||||||
|
.entities_by_type_and_remote_id
|
||||||
|
.contains_key(&id)
|
||||||
|
{
|
||||||
|
panic!("already subscribed to entity");
|
||||||
|
}
|
||||||
|
|
||||||
|
message_handlers.entities_by_type_and_remote_id.insert(
|
||||||
|
id,
|
||||||
|
EntityMessageSubscriber::Entity {
|
||||||
|
handle: entity.downgrade().into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request<T: RequestMessage>(
|
||||||
|
&self,
|
||||||
|
payload: T,
|
||||||
|
) -> 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);
|
||||||
|
async move {
|
||||||
|
let response = response.await?;
|
||||||
|
log::debug!("ssh request finish. name:{}", T::NAME);
|
||||||
|
T::Response::from_envelope(response)
|
||||||
|
.ok_or_else(|| anyhow!("received a response of the wrong type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl SshSocket {
|
pub fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
|
||||||
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
log::debug!("ssh send name:{}", T::NAME);
|
||||||
let mut command = process::Command::new("ssh");
|
self.send_dynamic(payload.into_envelope(0, None, None))
|
||||||
self.ssh_options(&mut command)
|
|
||||||
.arg(self.connection_options.ssh_url())
|
|
||||||
.arg(program);
|
|
||||||
command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
pub fn request_dynamic(
|
||||||
command
|
&self,
|
||||||
.stdin(Stdio::piped())
|
mut envelope: proto::Envelope,
|
||||||
.stdout(Stdio::piped())
|
type_name: &'static str,
|
||||||
.stderr(Stdio::piped())
|
) -> impl 'static + Future<Output = Result<proto::Envelope>> {
|
||||||
.args(["-o", "ControlMaster=no", "-o"])
|
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||||
.arg(format!("ControlPath={}", self.socket_path.display()))
|
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 = self.outgoing_tx.unbounded_send(envelope);
|
||||||
|
async move {
|
||||||
|
if let Err(error) = &result {
|
||||||
|
log::error!("failed to send message: {}", error);
|
||||||
|
return Err(anyhow!("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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_args(&self) -> Vec<String> {
|
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||||
vec![
|
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||||
"-o".to_string(),
|
self.outgoing_tx.unbounded_send(envelope)?;
|
||||||
"ControlMaster=no".to_string(),
|
Ok(())
|
||||||
"-o".to_string(),
|
|
||||||
format!("ControlPath={}", self.socket_path.display()),
|
|
||||||
self.connection_options.ssh_url(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
impl ProtoClient for ChannelClient {
|
||||||
let output = command.output().await?;
|
fn request(
|
||||||
if output.status.success() {
|
&self,
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
envelope: proto::Envelope,
|
||||||
} else {
|
request_type: &'static str,
|
||||||
Err(anyhow!(
|
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||||
"failed to run command: {}",
|
self.request_dynamic(envelope, request_type).boxed()
|
||||||
String::from_utf8_lossy(&output.stderr)
|
}
|
||||||
))
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use project::{
|
||||||
worktree_store::WorktreeStore,
|
worktree_store::WorktreeStore,
|
||||||
LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
|
LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
|
||||||
};
|
};
|
||||||
use remote::SshSession;
|
use remote::ssh_session::ChannelClient;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
|
proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
|
||||||
AnyProtoClient, TypedEnvelope,
|
AnyProtoClient, TypedEnvelope,
|
||||||
|
@ -41,7 +41,7 @@ impl HeadlessProject {
|
||||||
project::Project::init_settings(cx);
|
project::Project::init_settings(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
pub fn new(session: Arc<ChannelClient>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
||||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||||
|
|
||||||
let node_runtime = NodeRuntime::unavailable();
|
let node_runtime = NodeRuntime::unavailable();
|
||||||
|
|
|
@ -6,7 +6,6 @@ use gpui::Context as _;
|
||||||
use remote::{
|
use remote::{
|
||||||
json_log::LogRecord,
|
json_log::LogRecord,
|
||||||
protocol::{read_message, write_message},
|
protocol::{read_message, write_message},
|
||||||
SshSession,
|
|
||||||
};
|
};
|
||||||
use remote_server::HeadlessProject;
|
use remote_server::HeadlessProject;
|
||||||
use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async};
|
use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async};
|
||||||
|
@ -24,6 +23,8 @@ fn main() {
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn main() {
|
fn main() {
|
||||||
|
use remote::ssh_session::ChannelClient;
|
||||||
|
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
.format(|buf, record| {
|
.format(|buf, record| {
|
||||||
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
|
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
|
||||||
|
@ -55,7 +56,7 @@ fn main() {
|
||||||
let mut stdin = Async::new(io::stdin()).unwrap();
|
let mut stdin = Async::new(io::stdin()).unwrap();
|
||||||
let mut stdout = Async::new(io::stdout()).unwrap();
|
let mut stdout = Async::new(io::stdout()).unwrap();
|
||||||
|
|
||||||
let session = SshSession::server(incoming_rx, outgoing_tx, cx);
|
let session = ChannelClient::new(incoming_rx, outgoing_tx, cx);
|
||||||
let project = cx.new_model(|cx| {
|
let project = cx.new_model(|cx| {
|
||||||
HeadlessProject::new(
|
HeadlessProject::new(
|
||||||
session.clone(),
|
session.clone(),
|
||||||
|
|
|
@ -15,7 +15,7 @@ use project::{
|
||||||
search::{SearchQuery, SearchResult},
|
search::{SearchQuery, SearchResult},
|
||||||
Project, ProjectPath,
|
Project, ProjectPath,
|
||||||
};
|
};
|
||||||
use remote::SshSession;
|
use remote::SshRemoteClient;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{Settings, SettingsLocation, SettingsStore};
|
use settings::{Settings, SettingsLocation, SettingsStore};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
|
@ -616,7 +616,7 @@ async fn init_test(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
server_cx: &mut TestAppContext,
|
server_cx: &mut TestAppContext,
|
||||||
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
|
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
|
||||||
let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
|
let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
|
||||||
init_logger();
|
init_logger();
|
||||||
|
|
||||||
let fs = FakeFs::new(server_cx.executor());
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
@ -642,8 +642,9 @@ async fn init_test(
|
||||||
);
|
);
|
||||||
|
|
||||||
server_cx.update(HeadlessProject::init);
|
server_cx.update(HeadlessProject::init);
|
||||||
let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
|
let headless =
|
||||||
let project = build_project(client_ssh, cx);
|
server_cx.new_model(|cx| HeadlessProject::new(ssh_server_client, fs.clone(), cx));
|
||||||
|
let project = build_project(ssh_remote_client, cx);
|
||||||
|
|
||||||
project
|
project
|
||||||
.update(cx, {
|
.update(cx, {
|
||||||
|
@ -654,7 +655,7 @@ async fn init_test(
|
||||||
(project, headless, fs)
|
(project, headless, fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
|
fn build_project(ssh: Arc<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
|
|
|
@ -61,7 +61,7 @@ use postage::stream::Stream;
|
||||||
use project::{
|
use project::{
|
||||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||||
};
|
};
|
||||||
use remote::{SshConnectionOptions, SshSession};
|
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use session::AppSession;
|
use session::AppSession;
|
||||||
use settings::{InvalidSettingsError, Settings};
|
use settings::{InvalidSettingsError, Settings};
|
||||||
|
@ -5514,7 +5514,7 @@ pub fn join_hosted_project(
|
||||||
pub fn open_ssh_project(
|
pub fn open_ssh_project(
|
||||||
window: WindowHandle<Workspace>,
|
window: WindowHandle<Workspace>,
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
session: Arc<SshSession>,
|
session: Arc<SshRemoteClient>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
paths: Vec<PathBuf>,
|
paths: Vec<PathBuf>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue