ssh remote: Handle disconnect on project and show overlay (#19014)

Demo:



https://github.com/user-attachments/assets/e5edf8f3-8c15-482e-a792-6eb619f83de4


Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-10-10 12:59:09 +02:00 committed by GitHub
parent e3ff2ced79
commit b75532fad7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 264 additions and 78 deletions

View file

@ -1178,7 +1178,7 @@ impl Room {
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
!project.read(cx).is_disconnected()
!project.read(cx).is_disconnected(cx)
} else {
false
}

View file

@ -50,7 +50,7 @@ async fn test_channel_guests(
project_b.read_with(cx_b, |project, _| project.remote_id()),
Some(project_id),
);
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(project_b
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
@ -103,7 +103,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
workspace.active_item_as::<Editor>(cx).unwrap(),
)
});
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
assert!(room_b
@ -127,7 +127,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked();
// project and buffers are now editable
assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| !project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
// B sees themselves as muted, and can unmute.
@ -153,7 +153,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked();
// project and buffers are no longer editable
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
assert!(room_b
.update(cx_b, |room, cx| room.share_microphone(cx))

View file

@ -262,7 +262,7 @@ async fn test_dev_server_leave_room(
cx1.executor().run_until_parked();
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
}
#[gpui::test]
@ -308,7 +308,7 @@ async fn test_dev_server_delete(
cx1.executor().run_until_parked();
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
cx1.update(|cx| {
dev_server_projects::Store::global(cx).update(cx, |store, _| {
@ -418,12 +418,12 @@ async fn test_dev_server_refresh_access_token(
// Assert that the other client was disconnected
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
// Assert that the owner of the dev server does not see the dev server as online anymore
let (workspace, cx1) = client1.active_workspace(cx1);
cx1.update(|cx| {
assert!(workspace.read(cx).project().read(cx).is_disconnected());
assert!(workspace.read(cx).project().read(cx).is_disconnected(cx));
dev_server_projects::Store::global(cx).update(cx, |store, _| {
assert_eq!(
store.dev_servers().first().unwrap().status,

View file

@ -114,7 +114,7 @@ async fn test_host_disconnect(
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_b.read_with(cx_b, |project, _| project.is_read_only());
project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));

View file

@ -1389,7 +1389,7 @@ async fn test_unshare_project(
.unwrap();
executor.run_until_parked();
assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
assert!(project_b.read_with(cx_b, |project, cx| project.is_disconnected(cx)));
// Client C opens the project.
let project_c = client_c.join_remote_project(project_id, cx_c).await;
@ -1402,7 +1402,7 @@ async fn test_unshare_project(
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
assert!(project_c.read_with(cx_c, |project, cx| project.is_disconnected(cx)));
// Client C can open the project again after client A re-shares.
let project_id = active_call_a
@ -1427,8 +1427,8 @@ async fn test_unshare_project(
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_c2.read_with(cx_c, |project, _| {
assert!(project.is_disconnected());
project_c2.read_with(cx_c, |project, cx| {
assert!(project.is_disconnected(cx));
assert!(project.collaborators().is_empty());
});
}
@ -1560,8 +1560,8 @@ async fn test_project_reconnect(
assert_eq!(project.collaborators().len(), 1);
});
project_b1.read_with(cx_b, |project, _| {
assert!(!project.is_disconnected());
project_b1.read_with(cx_b, |project, cx| {
assert!(!project.is_disconnected(cx));
assert_eq!(project.collaborators().len(), 1);
});
@ -1661,7 +1661,7 @@ async fn test_project_reconnect(
});
project_b1.read_with(cx_b, |project, cx| {
assert!(!project.is_disconnected());
assert!(!project.is_disconnected(cx));
assert_eq!(
project
.worktree_for_id(worktree1_id, cx)
@ -1695,9 +1695,9 @@ async fn test_project_reconnect(
);
});
project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
project_b2.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected()));
project_b3.read_with(cx_b, |project, cx| assert!(!project.is_disconnected(cx)));
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
@ -1754,7 +1754,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
project_b1.read_with(cx_b, |project, cx| {
assert!(!project.is_disconnected());
assert!(!project.is_disconnected(cx));
assert_eq!(
project
.worktree_for_id(worktree1_id, cx)
@ -1788,7 +1788,7 @@ async fn test_project_reconnect(
);
});
project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
project_b3.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
@ -3816,8 +3816,8 @@ async fn test_leaving_project(
assert_eq!(project.collaborators().len(), 1);
});
project_b2.read_with(cx_b, |project, _| {
assert!(project.is_disconnected());
project_b2.read_with(cx_b, |project, cx| {
assert!(project.is_disconnected(cx));
});
project_c.read_with(cx_c, |project, _| {
@ -3849,12 +3849,12 @@ async fn test_leaving_project(
assert_eq!(project.collaborators().len(), 0);
});
project_b2.read_with(cx_b, |project, _| {
assert!(project.is_disconnected());
project_b2.read_with(cx_b, |project, cx| {
assert!(project.is_disconnected(cx));
});
project_c.read_with(cx_c, |project, _| {
assert!(project.is_disconnected());
project_c.read_with(cx_c, |project, cx| {
assert!(project.is_disconnected(cx));
});
}

View file

@ -1168,7 +1168,7 @@ impl RandomizedTest for ProjectCollaborationTest {
Some((project, cx))
});
if !guest_project.is_disconnected() {
if !guest_project.is_disconnected(cx) {
if let Some((host_project, host_cx)) = host_project {
let host_worktree_snapshots =
host_project.read_with(host_cx, |host_project, cx| {
@ -1254,8 +1254,8 @@ impl RandomizedTest for ProjectCollaborationTest {
let buffers = client.buffers().clone();
for (guest_project, guest_buffers) in &buffers {
let project_id = if guest_project.read_with(client_cx, |project, _| {
project.is_local() || project.is_disconnected()
let project_id = if guest_project.read_with(client_cx, |project, cx| {
project.is_local() || project.is_disconnected(cx)
}) {
continue;
} else {

View file

@ -532,9 +532,9 @@ impl<T: RandomizedTest> TestPlan<T> {
server.allow_connections();
for project in client.dev_server_projects().iter() {
project.read_with(&client_cx, |project, _| {
project.read_with(&client_cx, |project, cx| {
assert!(
project.is_disconnected(),
project.is_disconnected(cx),
"project {:?} should be read only",
project.remote_id()
)

View file

@ -647,17 +647,10 @@ pub struct FormattableBuffer {
}
pub struct RemoteLspStore {
upstream_client: AnyProtoClient,
upstream_client: Option<AnyProtoClient>,
upstream_project_id: u64,
}
impl RemoteLspStore {}
// pub struct SshLspStore {
// upstream_client: AnyProtoClient,
// current_lsp_settings: HashMap<LanguageServerName, LspSettings>,
// }
#[allow(clippy::large_enum_variant)]
pub enum LspStoreMode {
Local(LocalLspStore), // ssh host and collab host
@ -808,10 +801,15 @@ impl LspStore {
pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
match &self.mode {
LspStoreMode::Remote(RemoteLspStore {
upstream_client,
upstream_client: Some(upstream_client),
upstream_project_id,
..
}) => Some((upstream_client.clone(), *upstream_project_id)),
LspStoreMode::Remote(RemoteLspStore {
upstream_client: None,
..
}) => None,
LspStoreMode::Local(_) => None,
}
}
@ -924,7 +922,7 @@ impl LspStore {
Self {
mode: LspStoreMode::Remote(RemoteLspStore {
upstream_client,
upstream_client: Some(upstream_client),
upstream_project_id: project_id,
}),
downstream_client: None,
@ -3099,6 +3097,15 @@ impl LspStore {
self.downstream_client.take();
}
pub fn disconnected_from_ssh_remote(&mut self) {
if let LspStoreMode::Remote(RemoteLspStore {
upstream_client, ..
}) = &mut self.mode
{
upstream_client.take();
}
}
pub(crate) fn set_language_server_statuses_from_proto(
&mut self,
language_servers: Vec<proto::LanguageServer>,

View file

@ -58,7 +58,7 @@ use node_runtime::NodeRuntime;
use parking_lot::{Mutex, RwLock};
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::SshRemoteClient;
use remote::{SshConnectionOptions, SshRemoteClient};
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
use search::{SearchInputKind, SearchQuery, SearchResult};
use search_history::SearchHistory;
@ -245,6 +245,7 @@ pub enum Event {
},
RemoteIdChanged(Option<u64>),
DisconnectedFromHost,
DisconnectedFromSshRemote,
Closed,
DeletedEntry(ProjectEntryId),
CollaboratorUpdated {
@ -755,6 +756,8 @@ impl Project {
}
})
.detach();
cx.subscribe(&ssh, Self::on_ssh_event).detach();
cx.observe(&ssh, |_, _, cx| cx.notify()).detach();
let this = Self {
@ -1313,6 +1316,12 @@ impl Project {
.map(|ssh| ssh.read(cx).connection_state())
}
pub fn ssh_connection_options(&self, cx: &AppContext) -> Option<SshConnectionOptions> {
self.ssh_client
.as_ref()
.map(|ssh| ssh.read(cx).connection_options())
}
pub fn replica_id(&self) -> ReplicaId {
match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id,
@ -1658,7 +1667,7 @@ impl Project {
}
pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
if self.is_disconnected() {
if self.is_disconnected(cx) {
return;
}
self.disconnected_from_host_internal(cx);
@ -1708,16 +1717,24 @@ impl Project {
cx.emit(Event::Closed);
}
pub fn is_disconnected(&self) -> bool {
pub fn is_disconnected(&self, cx: &AppContext) -> bool {
match &self.client_state {
ProjectClientState::Remote {
sharing_has_stopped,
..
} => *sharing_has_stopped,
ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx),
_ => false,
}
}
fn ssh_is_disconnected(&self, cx: &AppContext) -> bool {
self.ssh_client
.as_ref()
.map(|ssh| ssh.read(cx).is_disconnected())
.unwrap_or(false)
}
pub fn capability(&self) -> Capability {
match &self.client_state {
ProjectClientState::Remote { capability, .. } => *capability,
@ -1725,8 +1742,8 @@ impl Project {
}
}
pub fn is_read_only(&self) -> bool {
self.is_disconnected() || self.capability() == Capability::ReadOnly
pub fn is_read_only(&self, cx: &AppContext) -> bool {
self.is_disconnected(cx) || self.capability() == Capability::ReadOnly
}
pub fn is_local(&self) -> bool {
@ -1807,7 +1824,7 @@ impl Project {
path: impl Into<ProjectPath>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
if self.is_via_collab() && self.is_disconnected() {
if (self.is_via_collab() || self.is_via_ssh()) && self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
@ -2114,6 +2131,30 @@ impl Project {
}
}
fn on_ssh_event(
&mut self,
_: Model<SshRemoteClient>,
event: &remote::SshRemoteEvent,
cx: &mut ModelContext<Self>,
) {
match event {
remote::SshRemoteEvent::Disconnected => {
// if self.is_via_ssh() {
// self.collaborators.clear();
self.worktree_store.update(cx, |store, cx| {
store.disconnected_from_host(cx);
});
self.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.disconnected_from_host(cx)
});
self.lsp_store.update(cx, |lsp_store, _cx| {
lsp_store.disconnected_from_ssh_remote()
});
cx.emit(Event::DisconnectedFromSshRemote);
}
}
}
fn on_settings_observer_event(
&mut self,
_: Model<SettingsObserver>,

View file

@ -509,6 +509,11 @@ impl WorktreeStore {
for worktree in &self.worktrees {
if let Some(worktree) = worktree.upgrade() {
worktree.update(cx, |worktree, _| {
println!(
"worktree. is_local: {:?}, is_remote: {:?}",
worktree.is_local(),
worktree.is_remote()
);
if let Some(worktree) = worktree.as_remote_mut() {
worktree.disconnected_from_host();
}

View file

@ -354,6 +354,7 @@ impl ProjectPanel {
});
}).ok();
let is_via_ssh = project.read(cx).is_via_ssh();
workspace
.open_path_preview(
@ -368,7 +369,11 @@ impl ProjectPanel {
)
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
match e.error_code() {
ErrorCode::Disconnected => Some("Disconnected from remote project".to_string()),
ErrorCode::Disconnected => if is_via_ssh {
Some("Disconnected from SSH host".to_string())
} else {
Some("Disconnected from remote project".to_string())
},
ErrorCode::UnsharedItem => Some(format!(
"{} is not shared by the host. This could be because it has been marked as `private`",
file_path.display()
@ -493,7 +498,7 @@ impl ProjectPanel {
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
let worktree_id = worktree.id();
let is_read_only = project.is_read_only();
let is_read_only = project.is_read_only(cx);
let is_remote = project.is_via_collab() && project.dev_server_project_id().is_none();
let is_local = project.is_local();
@ -2901,7 +2906,7 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::new_search_in_directory))
.on_action(cx.listener(Self::unfold_directory))
.on_action(cx.listener(Self::fold_directory))
.when(!project.is_read_only(), |el| {
.when(!project.is_read_only(cx), |el| {
el.on_action(cx.listener(Self::new_file))
.on_action(cx.listener(Self::new_directory))
.on_action(cx.listener(Self::rename))

View file

@ -1,19 +1,29 @@
use std::path::PathBuf;
use dev_server_projects::DevServer;
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
use remote::SshConnectionOptions;
use ui::{
div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, ViewContext,
};
use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace};
use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
use crate::{
dev_servers::reconnect_to_dev_server_project, open_dev_server_project, DevServerProjects,
dev_servers::reconnect_to_dev_server_project, open_dev_server_project, open_ssh_project,
DevServerProjects,
};
enum Host {
RemoteProject,
DevServerProject(DevServer),
SshRemoteProject(SshConnectionOptions),
}
pub struct DisconnectedOverlay {
workspace: WeakView<Workspace>,
dev_server: Option<DevServer>,
host: Host,
focus_handle: FocusHandle,
}
@ -32,7 +42,10 @@ impl ModalView for DisconnectedOverlay {
impl DisconnectedOverlay {
pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
cx.subscribe(workspace.project(), |workspace, project, event, cx| {
if !matches!(event, project::Event::DisconnectedFromHost) {
if !matches!(
event,
project::Event::DisconnectedFromHost | project::Event::DisconnectedFromSshRemote
) {
return;
}
let handle = cx.view().downgrade();
@ -45,9 +58,19 @@ impl DisconnectedOverlay {
.dev_server_for_project(id)
})
.cloned();
let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
let host = if let Some(dev_server) = dev_server {
Host::DevServerProject(dev_server)
} else if let Some(ssh_connection_options) = ssh_connection_options {
Host::SshRemoteProject(ssh_connection_options)
} else {
Host::RemoteProject
};
workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
workspace: handle,
dev_server,
host,
focus_handle: cx.focus_handle(),
});
})
@ -56,12 +79,22 @@ impl DisconnectedOverlay {
fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
match &self.host {
Host::DevServerProject(dev_server) => {
self.reconnect_to_dev_server(dev_server.clone(), cx);
}
Host::SshRemoteProject(ssh_connection_options) => {
self.reconnect_to_ssh_remote(ssh_connection_options.clone(), cx);
}
_ => {}
}
}
fn reconnect_to_dev_server(&self, dev_server: DevServer, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(dev_server) = self.dev_server.clone() else {
return;
};
let Some(dev_server_project_id) = workspace
.read(cx)
.project()
@ -102,6 +135,44 @@ impl DisconnectedOverlay {
}
}
fn reconnect_to_ssh_remote(
&self,
connection_options: SshConnectionOptions,
cx: &mut ViewContext<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
return;
};
let Some(window) = cx.window_handle().downcast::<Workspace>() else {
return;
};
let app_state = workspace.read(cx).app_state().clone();
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
cx.spawn(move |_, mut cx| async move {
open_ssh_project(
connection_options,
paths,
app_state,
OpenOptions {
replace_window: Some(window),
..Default::default()
},
&mut cx,
)
.await?;
Ok(())
})
.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent)
}
@ -109,6 +180,23 @@ impl DisconnectedOverlay {
impl Render for DisconnectedOverlay {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let can_reconnect = matches!(
self.host,
Host::DevServerProject(_) | Host::SshRemoteProject(_)
);
let message = match &self.host {
Host::RemoteProject | Host::DevServerProject(_) => {
"Your connection to the remote project has been lost.".to_string()
}
Host::SshRemoteProject(options) => {
format!(
"Your connection to {} has been lost",
options.connection_string()
)
}
};
div()
.track_focus(&self.focus_handle)
.elevation_3(cx)
@ -123,9 +211,7 @@ impl Render for DisconnectedOverlay {
.show_dismiss_button(true)
.child(Headline::new("Disconnected").size(HeadlineSize::Small)),
)
.section(Section::new().child(Label::new(
"Your connection to the remote project has been lost.",
)))
.section(Section::new().child(Label::new(message)))
.footer(
ModalFooter::new().end_slot(
h_flex()
@ -138,7 +224,7 @@ impl Render for DisconnectedOverlay {
cx.remove_window();
})),
)
.when_some(self.dev_server.clone(), |el, _| {
.when(can_reconnect, |el| {
el.child(
Button::new("reconnect", "Reconnect")
.style(ButtonStyle::Filled)

View file

@ -5,4 +5,5 @@ pub mod ssh_session;
pub use ssh_session::{
ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient,
SshRemoteEvent,
};

View file

@ -17,7 +17,8 @@ use futures::{
StreamExt as _,
};
use gpui::{
AppContext, AsyncAppContext, Context, Model, ModelContext, SemanticVersion, Task, WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task,
WeakModel,
};
use parking_lot::Mutex;
use rpc::{
@ -315,6 +316,10 @@ impl State {
matches!(self, Self::ReconnectFailed { .. })
}
fn is_reconnect_exhausted(&self) -> bool {
matches!(self, Self::ReconnectExhausted { .. })
}
fn is_reconnecting(&self) -> bool {
matches!(self, Self::Reconnecting { .. })
}
@ -376,7 +381,7 @@ impl State {
}
/// The state of the ssh connection.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConnectionState {
Connecting,
Connected,
@ -411,6 +416,13 @@ impl Drop for SshRemoteClient {
}
}
#[derive(Debug)]
pub enum SshRemoteEvent {
Disconnected,
}
impl EventEmitter<SshRemoteEvent> for SshRemoteClient {}
impl SshRemoteClient {
pub fn new(
unique_identifier: String,
@ -672,6 +684,9 @@ impl SshRemoteClient {
if this.state_is(State::is_reconnect_failed) {
this.reconnect(cx)
} else if this.state_is(State::is_reconnect_exhausted) {
cx.emit(SshRemoteEvent::Disconnected);
Ok(())
} else {
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state.");
Ok(())
@ -851,11 +866,15 @@ impl SshRemoteClient {
log::error!("failed to reconnect because server is not running");
this.update(&mut cx, |this, cx| {
this.set_state(State::ServerNotRunning, cx);
cx.emit(SshRemoteEvent::Disconnected);
})?;
}
}
} else if exit_code > 0 {
log::error!("proxy process terminated unexpectedly");
this.update(&mut cx, |this, cx| {
this.reconnect(cx).ok();
})?;
}
}
Ok(None) => {}
@ -963,6 +982,11 @@ impl SshRemoteClient {
self.connection_options.connection_string()
}
pub fn connection_options(&self) -> SshConnectionOptions {
self.connection_options.clone()
}
#[cfg(not(any(test, feature = "test-support")))]
pub fn connection_state(&self) -> ConnectionState {
self.state
.lock()
@ -971,6 +995,15 @@ impl SshRemoteClient {
.unwrap_or(ConnectionState::Disconnected)
}
#[cfg(any(test, feature = "test-support"))]
pub fn connection_state(&self) -> ConnectionState {
ConnectionState::Connected
}
pub fn is_disconnected(&self) -> bool {
self.connection_state() == ConnectionState::Disconnected
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(
client_cx: &mut gpui::TestAppContext,

View file

@ -366,7 +366,7 @@ impl TitleBar {
return self.render_ssh_project_host(cx);
}
if self.project.read(cx).is_disconnected() {
if self.project.read(cx).is_disconnected(cx) {
return Some(
Button::new("disconnected", "Disconnected")
.disabled(true)

View file

@ -823,6 +823,10 @@ impl Workspace {
}
}
project::Event::DisconnectedFromSshRemote => {
this.update_window_edited(cx);
}
project::Event::Closed => {
cx.remove_window();
}
@ -1464,6 +1468,10 @@ impl Workspace {
self.on_prompt_for_open_path = Some(prompt)
}
pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
self.serialized_ssh_project.clone()
}
pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
self.serialized_ssh_project = Some(serialized_ssh_project);
}
@ -1791,7 +1799,7 @@ impl Workspace {
mut save_intent: SaveIntent,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
if self.project.read(cx).is_disconnected() {
if self.project.read(cx).is_disconnected(cx) {
return Task::ready(Ok(true));
}
let dirty_items = self
@ -3447,7 +3455,7 @@ impl Workspace {
}
fn update_window_edited(&mut self, cx: &mut WindowContext) {
let is_edited = !self.project.read(cx).is_disconnected()
let is_edited = !self.project.read(cx).is_disconnected(cx)
&& self
.items(cx)
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
@ -4858,7 +4866,7 @@ impl Render for Workspace {
.children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
.children(if self.project.read(cx).is_disconnected(cx) {
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);