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:
Thorsten Ball 2024-10-01 12:16:44 +02:00 committed by GitHub
parent 527c9097f8
commit 7ce8797d78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 562 additions and 401 deletions

View file

@ -54,7 +54,7 @@ use parking_lot::{Mutex, RwLock};
use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::SshSession;
use remote::SshRemoteClient;
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
use search::{SearchInputKind, SearchQuery, SearchResult};
use search_history::SearchHistory;
@ -138,7 +138,7 @@ pub struct Project {
join_project_response_message_id: u32,
user_store: Model<UserStore>,
fs: Arc<dyn Fs>,
ssh_session: Option<Arc<SshSession>>,
ssh_client: Option<Arc<SshRemoteClient>>,
client_state: ProjectClientState,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
@ -643,7 +643,7 @@ impl Project {
user_store,
settings_observer,
fs,
ssh_session: None,
ssh_client: None,
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@ -664,7 +664,7 @@ impl Project {
}
pub fn ssh(
ssh: Arc<SshSession>,
ssh: Arc<SshRemoteClient>,
client: Arc<Client>,
node: NodeRuntime,
user_store: Model<UserStore>,
@ -682,14 +682,14 @@ impl Project {
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
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)
.detach();
let buffer_store = cx.new_model(|cx| {
BufferStore::remote(
worktree_store.clone(),
ssh.clone().into(),
ssh.to_proto_client(),
SSH_PROJECT_ID,
cx,
)
@ -698,7 +698,7 @@ impl Project {
.detach();
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)
.detach();
@ -709,7 +709,7 @@ impl Project {
buffer_store.clone(),
worktree_store.clone(),
languages.clone(),
ssh.clone().into(),
ssh.to_proto_client(),
SSH_PROJECT_ID,
cx,
)
@ -733,7 +733,7 @@ impl Project {
user_store,
settings_observer,
fs,
ssh_session: Some(ssh.clone()),
ssh_client: Some(ssh.clone()),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@ -751,7 +751,7 @@ impl Project {
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, &this.buffer_store);
@ -907,7 +907,7 @@ impl Project {
user_store: user_store.clone(),
snippets,
fs,
ssh_session: None,
ssh_client: None,
settings_observer: settings_observer.clone(),
client_subscriptions: Default::default(),
_subscriptions: vec![cx.on_release(Self::release)],
@ -1230,7 +1230,7 @@ impl Project {
match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id,
_ => {
if self.ssh_session.is_some() {
if self.ssh_client.is_some() {
1
} else {
0
@ -1638,7 +1638,7 @@ impl Project {
pub fn is_local(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
self.ssh_session.is_none()
self.ssh_client.is_none()
}
ProjectClientState::Remote { .. } => false,
}
@ -1647,7 +1647,7 @@ impl Project {
pub fn is_via_ssh(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
self.ssh_session.is_some()
self.ssh_client.is_some()
}
ProjectClientState::Remote { .. } => false,
}
@ -1933,8 +1933,9 @@ impl Project {
}
BufferStoreEvent::BufferChangedFilePath { .. } => {}
BufferStoreEvent::BufferDropped(buffer_id) => {
if let Some(ref ssh_session) = self.ssh_session {
ssh_session
if let Some(ref ssh_client) = self.ssh_client {
ssh_client
.to_proto_client()
.send(proto::CloseBuffer {
project_id: 0,
buffer_id: buffer_id.to_proto(),
@ -2139,13 +2140,14 @@ impl Project {
} => {
let operation = language::proto::serialize_operation(operation);
if let Some(ssh) = &self.ssh_session {
ssh.send(proto::UpdateBuffer {
project_id: 0,
buffer_id: buffer_id.to_proto(),
operations: vec![operation.clone()],
})
.ok();
if let Some(ssh) = &self.ssh_client {
ssh.to_proto_client()
.send(proto::UpdateBuffer {
project_id: 0,
buffer_id: buffer_id.to_proto(),
operations: vec![operation.clone()],
})
.ok();
}
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
@ -2825,14 +2827,13 @@ impl Project {
) -> Receiver<Model<Buffer>> {
let (tx, rx) = smol::channel::unbounded();
let (client, remote_id): (AnyProtoClient, _) =
if let Some(ssh_session) = self.ssh_session.clone() {
(ssh_session.into(), 0)
} else if let Some(remote_id) = self.remote_id() {
(self.client.clone().into(), remote_id)
} else {
return rx;
};
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
(ssh_client.to_proto_client(), 0)
} else if let Some(remote_id) = self.remote_id() {
(self.client.clone().into(), remote_id)
} else {
return rx;
};
let request = client.request(proto::FindSearchCandidates {
project_id: remote_id,
@ -2961,11 +2962,13 @@ impl Project {
exists.then(|| ResolvedPath::AbsPath(expanded))
})
} else if let Some(ssh_session) = self.ssh_session.as_ref() {
let request = ssh_session.request(proto::CheckFileExists {
project_id: SSH_PROJECT_ID,
path: path.to_string(),
});
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
let request = ssh_client
.to_proto_client()
.request(proto::CheckFileExists {
project_id: SSH_PROJECT_ID,
path: path.to_string(),
});
cx.background_executor().spawn(async move {
let response = request.await.log_err()?;
if response.exists {
@ -3035,13 +3038,13 @@ impl Project {
) -> Task<Result<Vec<PathBuf>>> {
if self.is_local() {
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 {
dev_server_id: SSH_PROJECT_ID,
path: query,
};
let response = session.request(request);
let response = session.to_proto_client().request(request);
cx.background_executor().spawn(async move {
let response = response.await?;
Ok(response.entries.into_iter().map(PathBuf::from).collect())
@ -3465,11 +3468,11 @@ impl Project {
cx: AsyncAppContext,
) -> Result<proto::Ack> {
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();
payload.project_id = 0;
cx.background_executor()
.spawn(ssh.request(payload))
.spawn(ssh.to_proto_client().request(payload))
.detach_and_log_err(cx);
}
this.buffer_store.clone()