ssh remoting: Enable reconnecting after connection losses (#18586)
Release Notes: - N/A --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
67fbdbbed6
commit
c03b8d6c48
19 changed files with 727 additions and 240 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -9165,6 +9165,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
|
"clap",
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
@ -14324,6 +14325,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
"release_channel",
|
||||||
"remote",
|
"remote",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -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<SshRemoteClient>,
|
ssh: Model<SshRemoteClient>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (Model<Project>, WorktreeId) {
|
) -> (Model<Project>, WorktreeId) {
|
||||||
let project = cx.update(|cx| {
|
let project = cx.update(|cx| {
|
||||||
|
|
|
@ -141,7 +141,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_client: Option<Arc<SshRemoteClient>>,
|
ssh_client: Option<Model<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>,
|
||||||
|
@ -667,7 +667,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ssh(
|
pub fn ssh(
|
||||||
ssh: Arc<SshRemoteClient>,
|
ssh: Model<SshRemoteClient>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
node: NodeRuntime,
|
node: NodeRuntime,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
|
@ -684,15 +684,16 @@ impl Project {
|
||||||
let snippets =
|
let snippets =
|
||||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||||
|
|
||||||
|
let ssh_proto = ssh.read(cx).to_proto_client();
|
||||||
let worktree_store =
|
let worktree_store =
|
||||||
cx.new_model(|_| WorktreeStore::remote(false, ssh.to_proto_client(), 0, None));
|
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 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.to_proto_client(),
|
ssh.read(cx).to_proto_client(),
|
||||||
SSH_PROJECT_ID,
|
SSH_PROJECT_ID,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -701,7 +702,7 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let settings_observer = cx.new_model(|cx| {
|
let settings_observer = cx.new_model(|cx| {
|
||||||
SettingsObserver::new_ssh(ssh.to_proto_client(), worktree_store.clone(), cx)
|
SettingsObserver::new_ssh(ssh_proto.clone(), worktree_store.clone(), cx)
|
||||||
});
|
});
|
||||||
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -712,13 +713,24 @@ impl Project {
|
||||||
buffer_store.clone(),
|
buffer_store.clone(),
|
||||||
worktree_store.clone(),
|
worktree_store.clone(),
|
||||||
languages.clone(),
|
languages.clone(),
|
||||||
ssh.to_proto_client(),
|
ssh_proto.clone(),
|
||||||
SSH_PROJECT_ID,
|
SSH_PROJECT_ID,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||||
|
|
||||||
|
cx.on_release(|this, cx| {
|
||||||
|
if let Some(ssh_client) = this.ssh_client.as_ref() {
|
||||||
|
ssh_client
|
||||||
|
.read(cx)
|
||||||
|
.to_proto_client()
|
||||||
|
.send(proto::ShutdownRemoteServer {})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
let this = Self {
|
let this = Self {
|
||||||
buffer_ordered_messages_tx: tx,
|
buffer_ordered_messages_tx: tx,
|
||||||
collaborators: Default::default(),
|
collaborators: Default::default(),
|
||||||
|
@ -754,20 +766,20 @@ impl Project {
|
||||||
search_excluded_history: Self::new_search_history(),
|
search_excluded_history: Self::new_search_history(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let client: AnyProtoClient = ssh.to_proto_client();
|
let ssh = ssh.read(cx);
|
||||||
|
|
||||||
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);
|
||||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
|
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
|
||||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
|
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
|
||||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
|
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
|
||||||
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
|
||||||
client.add_model_message_handler(Self::handle_update_worktree);
|
ssh_proto.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
||||||
client.add_model_message_handler(Self::handle_update_project);
|
ssh_proto.add_model_message_handler(Self::handle_update_worktree);
|
||||||
client.add_model_request_handler(BufferStore::handle_update_buffer);
|
ssh_proto.add_model_message_handler(Self::handle_update_project);
|
||||||
BufferStore::init(&client);
|
ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer);
|
||||||
LspStore::init(&client);
|
BufferStore::init(&ssh_proto);
|
||||||
SettingsObserver::init(&client);
|
LspStore::init(&ssh_proto);
|
||||||
|
SettingsObserver::init(&ssh_proto);
|
||||||
|
|
||||||
this
|
this
|
||||||
})
|
})
|
||||||
|
@ -1222,7 +1234,7 @@ impl Project {
|
||||||
|
|
||||||
pub fn ssh_connection_string(&self, cx: &AppContext) -> Option<SharedString> {
|
pub fn ssh_connection_string(&self, cx: &AppContext) -> Option<SharedString> {
|
||||||
if let Some(ssh_state) = &self.ssh_client {
|
if let Some(ssh_state) = &self.ssh_client {
|
||||||
return Some(ssh_state.connection_string().into());
|
return Some(ssh_state.read(cx).connection_string().into());
|
||||||
}
|
}
|
||||||
let dev_server_id = self.dev_server_project_id()?;
|
let dev_server_id = self.dev_server_project_id()?;
|
||||||
dev_server_projects::Store::global(cx)
|
dev_server_projects::Store::global(cx)
|
||||||
|
@ -1232,8 +1244,8 @@ impl Project {
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ssh_is_connected(&self) -> Option<bool> {
|
pub fn ssh_is_connected(&self, cx: &AppContext) -> Option<bool> {
|
||||||
Some(!self.ssh_client.as_ref()?.is_reconnect_underway())
|
Some(!self.ssh_client.as_ref()?.read(cx).is_reconnect_underway())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replica_id(&self) -> ReplicaId {
|
pub fn replica_id(&self) -> ReplicaId {
|
||||||
|
@ -1945,6 +1957,7 @@ impl Project {
|
||||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||||
if let Some(ref ssh_client) = self.ssh_client {
|
if let Some(ref ssh_client) = self.ssh_client {
|
||||||
ssh_client
|
ssh_client
|
||||||
|
.read(cx)
|
||||||
.to_proto_client()
|
.to_proto_client()
|
||||||
.send(proto::CloseBuffer {
|
.send(proto::CloseBuffer {
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
|
@ -2151,7 +2164,8 @@ impl Project {
|
||||||
let operation = language::proto::serialize_operation(operation);
|
let operation = language::proto::serialize_operation(operation);
|
||||||
|
|
||||||
if let Some(ssh) = &self.ssh_client {
|
if let Some(ssh) = &self.ssh_client {
|
||||||
ssh.to_proto_client()
|
ssh.read(cx)
|
||||||
|
.to_proto_client()
|
||||||
.send(proto::UpdateBuffer {
|
.send(proto::UpdateBuffer {
|
||||||
project_id: 0,
|
project_id: 0,
|
||||||
buffer_id: buffer_id.to_proto(),
|
buffer_id: buffer_id.to_proto(),
|
||||||
|
@ -2838,7 +2852,7 @@ impl Project {
|
||||||
let (tx, rx) = smol::channel::unbounded();
|
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.ssh_client {
|
||||||
(ssh_client.to_proto_client(), 0)
|
(ssh_client.read(cx).to_proto_client(), 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 {
|
||||||
|
@ -2973,7 +2987,9 @@ impl Project {
|
||||||
exists.then(|| ResolvedPath::AbsPath(expanded))
|
exists.then(|| ResolvedPath::AbsPath(expanded))
|
||||||
})
|
})
|
||||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||||
let request = ssh_client
|
let request =
|
||||||
|
ssh_client
|
||||||
|
.read(cx)
|
||||||
.to_proto_client()
|
.to_proto_client()
|
||||||
.request(proto::CheckFileExists {
|
.request(proto::CheckFileExists {
|
||||||
project_id: SSH_PROJECT_ID,
|
project_id: SSH_PROJECT_ID,
|
||||||
|
@ -3054,7 +3070,7 @@ impl Project {
|
||||||
path: query,
|
path: query,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = session.to_proto_client().request(request);
|
let response = session.read(cx).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())
|
||||||
|
@ -3482,7 +3498,7 @@ impl Project {
|
||||||
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.to_proto_client().request(payload))
|
.spawn(ssh.read(cx).to_proto_client().request(payload))
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
this.buffer_store.clone()
|
this.buffer_store.clone()
|
||||||
|
|
|
@ -70,7 +70,7 @@ impl Project {
|
||||||
if let Some(args) = self
|
if let Some(args) = self
|
||||||
.ssh_client
|
.ssh_client
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|session| session.ssh_args())
|
.and_then(|session| session.read(cx).ssh_args())
|
||||||
{
|
{
|
||||||
return Some(SshCommand::Direct(args));
|
return Some(SshCommand::Direct(args));
|
||||||
}
|
}
|
||||||
|
|
|
@ -282,7 +282,9 @@ message Envelope {
|
||||||
UpdateUserSettings update_user_settings = 246;
|
UpdateUserSettings update_user_settings = 246;
|
||||||
|
|
||||||
CheckFileExists check_file_exists = 255;
|
CheckFileExists check_file_exists = 255;
|
||||||
CheckFileExistsResponse check_file_exists_response = 256; // current max
|
CheckFileExistsResponse check_file_exists_response = 256;
|
||||||
|
|
||||||
|
ShutdownRemoteServer shutdown_remote_server = 257; // current max
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 87 to 88;
|
reserved 87 to 88;
|
||||||
|
@ -2511,3 +2513,5 @@ message CheckFileExistsResponse {
|
||||||
bool exists = 1;
|
bool exists = 1;
|
||||||
string path = 2;
|
string path = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ShutdownRemoteServer {}
|
||||||
|
|
|
@ -364,7 +364,8 @@ messages!(
|
||||||
(CloseBuffer, Foreground),
|
(CloseBuffer, Foreground),
|
||||||
(UpdateUserSettings, Foreground),
|
(UpdateUserSettings, Foreground),
|
||||||
(CheckFileExists, Background),
|
(CheckFileExists, Background),
|
||||||
(CheckFileExistsResponse, Background)
|
(CheckFileExistsResponse, Background),
|
||||||
|
(ShutdownRemoteServer, Foreground),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -487,7 +488,8 @@ request_messages!(
|
||||||
(SynchronizeContexts, SynchronizeContextsResponse),
|
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||||
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
||||||
(AddWorktree, AddWorktreeResponse),
|
(AddWorktree, AddWorktreeResponse),
|
||||||
(CheckFileExists, CheckFileExistsResponse)
|
(CheckFileExists, CheckFileExistsResponse),
|
||||||
|
(ShutdownRemoteServer, Ack)
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
|
|
@ -305,12 +305,18 @@ impl DevServerProjects {
|
||||||
|
|
||||||
let connection_options = remote::SshConnectionOptions {
|
let connection_options = remote::SshConnectionOptions {
|
||||||
host: host.to_string(),
|
host: host.to_string(),
|
||||||
username,
|
username: username.clone(),
|
||||||
port,
|
port,
|
||||||
password: None,
|
password: None,
|
||||||
};
|
};
|
||||||
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
|
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
|
||||||
let connection = connect_over_ssh(connection_options.clone(), ssh_prompt.clone(), cx)
|
|
||||||
|
let connection = connect_over_ssh(
|
||||||
|
connection_options.dev_server_identifier(),
|
||||||
|
connection_options.clone(),
|
||||||
|
ssh_prompt.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
.prompt_err("Failed to connect", cx, |_, _| None);
|
.prompt_err("Failed to connect", cx, |_, _| None);
|
||||||
|
|
||||||
let creating = cx.spawn(move |this, mut cx| async move {
|
let creating = cx.spawn(move |this, mut cx| async move {
|
||||||
|
@ -363,11 +369,13 @@ impl DevServerProjects {
|
||||||
.prompt
|
.prompt
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let connect = connect_over_ssh(connection_options, prompt, cx).prompt_err(
|
let connect = connect_over_ssh(
|
||||||
"Failed to connect",
|
connection_options.dev_server_identifier(),
|
||||||
|
connection_options,
|
||||||
|
prompt,
|
||||||
cx,
|
cx,
|
||||||
|_, _| None,
|
)
|
||||||
);
|
.prompt_err("Failed to connect", cx, |_, _| None);
|
||||||
cx.spawn(|workspace, mut cx| async move {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
let Some(session) = connect.await else {
|
let Some(session) = connect.await else {
|
||||||
workspace
|
workspace
|
||||||
|
|
|
@ -4,12 +4,12 @@ use anyhow::Result;
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::AppContext;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
||||||
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
|
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
|
||||||
Transformation, View,
|
Transformation, View,
|
||||||
};
|
};
|
||||||
|
use gpui::{AppContext, Model};
|
||||||
use release_channel::{AppVersion, ReleaseChannel};
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -373,25 +373,24 @@ impl SshClientDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect_over_ssh(
|
pub fn connect_over_ssh(
|
||||||
|
unique_identifier: String,
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
ui: View<SshPrompt>,
|
ui: View<SshPrompt>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<Arc<SshRemoteClient>>> {
|
) -> Task<Result<Model<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 {
|
|
||||||
remote::SshRemoteClient::new(
|
remote::SshRemoteClient::new(
|
||||||
|
unique_identifier,
|
||||||
connection_options,
|
connection_options,
|
||||||
Arc::new(SshClientDelegate {
|
Arc::new(SshClientDelegate {
|
||||||
window,
|
window,
|
||||||
ui,
|
ui,
|
||||||
known_password,
|
known_password,
|
||||||
}),
|
}),
|
||||||
&mut cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_ssh_project(
|
pub async fn open_ssh_project(
|
||||||
|
@ -420,8 +419,7 @@ pub async fn open_ssh_project(
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
let session = window
|
let delegate = window.update(cx, |workspace, cx| {
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
cx.activate_window();
|
cx.activate_window();
|
||||||
workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
|
workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
|
||||||
let ui = workspace
|
let ui = workspace
|
||||||
|
@ -430,12 +428,16 @@ pub async fn open_ssh_project(
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.prompt
|
.prompt
|
||||||
.clone();
|
.clone();
|
||||||
connect_over_ssh(connection_options.clone(), ui, cx)
|
|
||||||
})?
|
Arc::new(SshClientDelegate {
|
||||||
.await?;
|
window: cx.window_handle(),
|
||||||
|
ui,
|
||||||
|
known_password: connection_options.password.clone(),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
workspace::open_ssh_project(window, connection_options, session, app_state, paths, cx)
|
workspace::open_ssh_project(window, connection_options, delegate, app_state, paths, cx)
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,3 +49,17 @@ pub async fn write_message<S: AsyncWrite + Unpin>(
|
||||||
stream.write_all(buffer).await?;
|
stream.write_all(buffer).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn read_message_raw<S: AsyncRead + Unpin>(
|
||||||
|
stream: &mut S,
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
) -> Result<()> {
|
||||||
|
buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||||
|
stream.read_exact(buffer).await?;
|
||||||
|
|
||||||
|
let message_len = message_len_from_buffer(buffer);
|
||||||
|
buffer.resize(message_len as usize, 0);
|
||||||
|
stream.read_exact(buffer).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,9 @@ use futures::{
|
||||||
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
|
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
|
||||||
StreamExt as _,
|
StreamExt as _,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, Task};
|
use gpui::{
|
||||||
|
AppContext, AsyncAppContext, Context, Model, ModelContext, SemanticVersion, Task, WeakModel,
|
||||||
|
};
|
||||||
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},
|
||||||
|
@ -28,10 +30,11 @@ use smol::{
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
|
mem,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU32, Ordering::SeqCst},
|
atomic::{AtomicU32, Ordering::SeqCst},
|
||||||
Arc, Weak,
|
Arc,
|
||||||
},
|
},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
@ -92,6 +95,17 @@ impl SshConnectionOptions {
|
||||||
host
|
host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uniquely identifies dev server projects on a remote host. Needs to be
|
||||||
|
// stable for the same dev server project.
|
||||||
|
pub fn dev_server_identifier(&self) -> String {
|
||||||
|
let mut identifier = format!("dev-server-{:?}", self.host);
|
||||||
|
if let Some(username) = self.username.as_ref() {
|
||||||
|
identifier.push('-');
|
||||||
|
identifier.push_str(&username);
|
||||||
|
}
|
||||||
|
identifier
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
@ -250,39 +264,62 @@ struct SshRemoteClientState {
|
||||||
|
|
||||||
pub struct SshRemoteClient {
|
pub struct SshRemoteClient {
|
||||||
client: Arc<ChannelClient>,
|
client: Arc<ChannelClient>,
|
||||||
inner_state: Mutex<Option<SshRemoteClientState>>,
|
unique_identifier: String,
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
|
inner_state: Arc<Mutex<Option<SshRemoteClientState>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SshRemoteClient {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.shutdown_processes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshRemoteClient {
|
impl SshRemoteClient {
|
||||||
pub async fn new(
|
pub fn new(
|
||||||
|
unique_identifier: String,
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &AppContext,
|
||||||
) -> Result<Arc<Self>> {
|
) -> Task<Result<Model<Self>>> {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
|
||||||
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
|
let this = cx.new_model(|cx| {
|
||||||
let this = Arc::new(Self {
|
cx.on_app_quit(|this: &mut Self, _| {
|
||||||
|
this.shutdown_processes();
|
||||||
|
futures::future::ready(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let client = ChannelClient::new(incoming_rx, outgoing_tx, cx);
|
||||||
|
Self {
|
||||||
client,
|
client,
|
||||||
inner_state: Mutex::new(None),
|
unique_identifier: unique_identifier.clone(),
|
||||||
connection_options: connection_options.clone(),
|
connection_options: SshConnectionOptions::default(),
|
||||||
});
|
inner_state: Arc::new(Mutex::new(None)),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let inner_state = {
|
let inner_state = {
|
||||||
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||||
ChannelForwarder::new(incoming_tx, outgoing_rx, cx);
|
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||||
|
|
||||||
let (ssh_connection, ssh_process) =
|
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
|
||||||
Self::establish_connection(connection_options, delegate.clone(), cx).await?;
|
unique_identifier,
|
||||||
|
connection_options,
|
||||||
|
delegate.clone(),
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let multiplex_task = Self::multiplex(
|
let multiplex_task = Self::multiplex(
|
||||||
Arc::downgrade(&this),
|
this.downgrade(),
|
||||||
ssh_process,
|
ssh_proxy_process,
|
||||||
proxy_incoming_tx,
|
proxy_incoming_tx,
|
||||||
proxy_outgoing_rx,
|
proxy_outgoing_rx,
|
||||||
cx,
|
&mut cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
SshRemoteClientState {
|
SshRemoteClientState {
|
||||||
|
@ -293,16 +330,35 @@ impl SshRemoteClient {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
this.inner_state.lock().replace(inner_state);
|
this.inner_state.lock().replace(inner_state);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(this)
|
Ok(this)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reconnect(this: Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
fn shutdown_processes(&self) {
|
||||||
let Some(state) = this.inner_state.lock().take() else {
|
let Some(mut state) = self.inner_state.lock().take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log::info!("shutting down ssh processes");
|
||||||
|
// Drop `multiplex_task` because it owns our ssh_proxy_process, which is a
|
||||||
|
// child of master_process.
|
||||||
|
let task = mem::replace(&mut state.multiplex_task, Task::ready(Ok(())));
|
||||||
|
drop(task);
|
||||||
|
// Now drop the rest of state, which kills master process.
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconnect(&self, cx: &ModelContext<Self>) -> Result<()> {
|
||||||
|
let Some(state) = self.inner_state.lock().take() else {
|
||||||
return Err(anyhow!("reconnect is already in progress"));
|
return Err(anyhow!("reconnect is already in progress"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let workspace_identifier = self.unique_identifier.clone();
|
||||||
|
|
||||||
let SshRemoteClientState {
|
let SshRemoteClientState {
|
||||||
mut ssh_connection,
|
mut ssh_connection,
|
||||||
delegate,
|
delegate,
|
||||||
|
@ -311,7 +367,7 @@ impl SshRemoteClient {
|
||||||
} = state;
|
} = state;
|
||||||
drop(multiplex_task);
|
drop(multiplex_task);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let (incoming_tx, outgoing_rx) = proxy.into_channels().await;
|
let (incoming_tx, outgoing_rx) = proxy.into_channels().await;
|
||||||
|
|
||||||
ssh_connection.master_process.kill()?;
|
ssh_connection.master_process.kill()?;
|
||||||
|
@ -323,8 +379,13 @@ impl SshRemoteClient {
|
||||||
|
|
||||||
let connection_options = ssh_connection.socket.connection_options.clone();
|
let connection_options = ssh_connection.socket.connection_options.clone();
|
||||||
|
|
||||||
let (ssh_connection, ssh_process) =
|
let (ssh_connection, ssh_process) = Self::establish_connection(
|
||||||
Self::establish_connection(connection_options, delegate.clone(), &mut cx).await?;
|
workspace_identifier,
|
||||||
|
connection_options,
|
||||||
|
delegate.clone(),
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||||
|
@ -334,32 +395,32 @@ impl SshRemoteClient {
|
||||||
delegate,
|
delegate,
|
||||||
forwarder: proxy,
|
forwarder: proxy,
|
||||||
multiplex_task: Self::multiplex(
|
multiplex_task: Self::multiplex(
|
||||||
Arc::downgrade(&this),
|
this.clone(),
|
||||||
ssh_process,
|
ssh_process,
|
||||||
proxy_incoming_tx,
|
proxy_incoming_tx,
|
||||||
proxy_outgoing_rx,
|
proxy_outgoing_rx,
|
||||||
&mut cx,
|
&mut cx,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
this.inner_state.lock().replace(inner_state);
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.inner_state.lock().replace(inner_state);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
Ok(())
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn multiplex(
|
fn multiplex(
|
||||||
this: Weak<Self>,
|
this: WeakModel<Self>,
|
||||||
mut ssh_process: Child,
|
mut ssh_proxy_process: Child,
|
||||||
incoming_tx: UnboundedSender<Envelope>,
|
incoming_tx: UnboundedSender<Envelope>,
|
||||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||||
cx: &AsyncAppContext,
|
cx: &AsyncAppContext,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let mut child_stderr = ssh_process.stderr.take().unwrap();
|
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
|
||||||
let mut child_stdout = ssh_process.stdout.take().unwrap();
|
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
|
||||||
let mut child_stdin = ssh_process.stdin.take().unwrap();
|
let mut child_stdin = ssh_proxy_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();
|
||||||
|
@ -385,7 +446,7 @@ impl SshRemoteClient {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
child_stdin.close().await?;
|
child_stdin.close().await?;
|
||||||
outgoing_rx.close();
|
outgoing_rx.close();
|
||||||
let status = ssh_process.status().await?;
|
let status = ssh_proxy_process.status().await?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
log::error!("ssh process 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 Err(anyhow!("ssh process exited with non-zero status code: {:?}", status.code()));
|
||||||
|
@ -446,9 +507,9 @@ impl SshRemoteClient {
|
||||||
|
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
|
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
|
||||||
if let Some(this) = this.upgrade() {
|
this.update(&mut cx, |this, cx| {
|
||||||
Self::reconnect(this, &mut cx).ok();
|
this.reconnect(cx).ok();
|
||||||
}
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -456,6 +517,7 @@ impl SshRemoteClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn establish_connection(
|
async fn establish_connection(
|
||||||
|
unique_identifier: String,
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
|
@ -479,17 +541,22 @@ impl SshRemoteClient {
|
||||||
let socket = ssh_connection.socket.clone();
|
let socket = ssh_connection.socket.clone();
|
||||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||||
|
|
||||||
let ssh_process = socket
|
delegate.set_status(Some("Starting proxy"), cx);
|
||||||
|
|
||||||
|
let ssh_proxy_process = socket
|
||||||
.ssh_command(format!(
|
.ssh_command(format!(
|
||||||
"RUST_LOG={} RUST_BACKTRACE={} {:?} run",
|
"RUST_LOG={} RUST_BACKTRACE={} {:?} proxy --identifier {}",
|
||||||
std::env::var("RUST_LOG").unwrap_or_default(),
|
std::env::var("RUST_LOG").unwrap_or_default(),
|
||||||
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
|
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
|
||||||
remote_binary_path,
|
remote_binary_path,
|
||||||
|
unique_identifier,
|
||||||
))
|
))
|
||||||
|
// IMPORTANT: we kill this process when we drop the task that uses it.
|
||||||
|
.kill_on_drop(true)
|
||||||
.spawn()
|
.spawn()
|
||||||
.context("failed to spawn remote server")?;
|
.context("failed to spawn remote server")?;
|
||||||
|
|
||||||
Ok((ssh_connection, ssh_process))
|
Ok((ssh_connection, ssh_proxy_process))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||||
|
@ -514,21 +581,25 @@ impl SshRemoteClient {
|
||||||
pub fn is_reconnect_underway(&self) -> bool {
|
pub fn is_reconnect_underway(&self) -> bool {
|
||||||
maybe!({ Some(self.inner_state.try_lock()?.is_none()) }).unwrap_or_default()
|
maybe!({ Some(self.inner_state.try_lock()?.is_none()) }).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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<ChannelClient>) {
|
) -> (Model<Self>, Arc<ChannelClient>) {
|
||||||
|
use gpui::Context;
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
(
|
(
|
||||||
client_cx.update(|cx| {
|
client_cx.update(|cx| {
|
||||||
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
|
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
|
||||||
Arc::new(Self {
|
cx.new_model(|_| Self {
|
||||||
client,
|
client,
|
||||||
inner_state: Mutex::new(None),
|
unique_identifier: "fake".to_string(),
|
||||||
connection_options: SshConnectionOptions::default(),
|
connection_options: SshConnectionOptions::default(),
|
||||||
|
inner_state: Arc::new(Mutex::new(None)),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
|
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
|
||||||
|
|
|
@ -22,25 +22,26 @@ test-support = ["fs/test-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
node_runtime.workspace = true
|
language.workspace = true
|
||||||
|
languages.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
node_runtime.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
remote.workspace = true
|
remote.workspace = true
|
||||||
rpc.workspace = true
|
rpc.workspace = true
|
||||||
settings.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
shellexpand.workspace = true
|
shellexpand.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
worktree.workspace = true
|
|
||||||
language.workspace = true
|
|
||||||
languages.workspace = true
|
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
worktree.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { workspace = true, features = ["test-support"] }
|
client = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -112,6 +112,7 @@ impl HeadlessProject {
|
||||||
|
|
||||||
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
|
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
|
||||||
client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists);
|
client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists);
|
||||||
|
client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server);
|
||||||
|
|
||||||
client.add_model_request_handler(Self::handle_add_worktree);
|
client.add_model_request_handler(Self::handle_add_worktree);
|
||||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||||
|
@ -335,4 +336,22 @@ impl HeadlessProject {
|
||||||
path: expanded,
|
path: expanded,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_shutdown_remote_server(
|
||||||
|
_this: Model<Self>,
|
||||||
|
_envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::Ack> {
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
cx.update(|cx| {
|
||||||
|
// TODO: This is a hack, because in a headless project, shutdown isn't executed
|
||||||
|
// when calling quit, but it should be.
|
||||||
|
cx.shutdown();
|
||||||
|
cx.quit();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(proto::Ack {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,34 @@
|
||||||
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
||||||
|
|
||||||
use fs::RealFs;
|
use anyhow::Result;
|
||||||
use futures::channel::mpsc;
|
use clap::{Parser, Subcommand};
|
||||||
use gpui::Context as _;
|
use std::path::PathBuf;
|
||||||
use remote::{
|
|
||||||
json_log::LogRecord,
|
#[derive(Parser)]
|
||||||
protocol::{read_message, write_message},
|
#[command(disable_version_flag = true)]
|
||||||
};
|
struct Cli {
|
||||||
use remote_server::HeadlessProject;
|
#[command(subcommand)]
|
||||||
use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async};
|
command: Option<Commands>,
|
||||||
use std::{
|
}
|
||||||
env,
|
|
||||||
io::{self, Write},
|
#[derive(Subcommand)]
|
||||||
mem, process,
|
enum Commands {
|
||||||
sync::Arc,
|
Run {
|
||||||
};
|
#[arg(long)]
|
||||||
|
log_file: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
pid_file: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
stdin_socket: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
stdout_socket: PathBuf,
|
||||||
|
},
|
||||||
|
Proxy {
|
||||||
|
#[arg(long)]
|
||||||
|
identifier: String,
|
||||||
|
},
|
||||||
|
Version,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -22,76 +36,32 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn main() {
|
fn main() -> Result<()> {
|
||||||
use remote::ssh_session::ChannelClient;
|
use remote_server::unix::{execute_proxy, execute_run, init_logging};
|
||||||
|
|
||||||
env_logger::builder()
|
let cli = Cli::parse();
|
||||||
.format(|buf, record| {
|
|
||||||
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
|
match cli.command {
|
||||||
buf.write_all(b"\n")?;
|
Some(Commands::Run {
|
||||||
|
log_file,
|
||||||
|
pid_file,
|
||||||
|
stdin_socket,
|
||||||
|
stdout_socket,
|
||||||
|
}) => {
|
||||||
|
init_logging(Some(log_file))?;
|
||||||
|
execute_run(pid_file, stdin_socket, stdout_socket)
|
||||||
|
}
|
||||||
|
Some(Commands::Proxy { identifier }) => {
|
||||||
|
init_logging(None)?;
|
||||||
|
execute_proxy(identifier)
|
||||||
|
}
|
||||||
|
Some(Commands::Version) => {
|
||||||
|
eprintln!("{}", env!("ZED_PKG_VERSION"));
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let subcommand = std::env::args().nth(1);
|
|
||||||
match subcommand.as_deref() {
|
|
||||||
Some("run") => {}
|
|
||||||
Some("version") => {
|
|
||||||
println!("{}", env!("ZED_PKG_VERSION"));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
_ => {
|
None => {
|
||||||
eprintln!("usage: remote <run|version>");
|
eprintln!("usage: remote <run|proxy|version>");
|
||||||
process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gpui::App::headless().run(move |cx| {
|
|
||||||
settings::init(cx);
|
|
||||||
HeadlessProject::init(cx);
|
|
||||||
|
|
||||||
let (incoming_tx, incoming_rx) = mpsc::unbounded();
|
|
||||||
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
|
|
||||||
|
|
||||||
let mut stdin = Async::new(io::stdin()).unwrap();
|
|
||||||
let mut stdout = Async::new(io::stdout()).unwrap();
|
|
||||||
|
|
||||||
let session = ChannelClient::new(incoming_rx, outgoing_tx, cx);
|
|
||||||
let project = cx.new_model(|cx| {
|
|
||||||
HeadlessProject::new(
|
|
||||||
session.clone(),
|
|
||||||
Arc::new(RealFs::new(Default::default(), None)),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
let mut output_buffer = Vec::new();
|
|
||||||
while let Some(message) = outgoing_rx.next().await {
|
|
||||||
write_message(&mut stdout, &mut output_buffer, message).await?;
|
|
||||||
stdout.flush().await?;
|
|
||||||
}
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
let mut input_buffer = Vec::new();
|
|
||||||
loop {
|
|
||||||
let message = match read_message(&mut stdin, &mut input_buffer).await {
|
|
||||||
Ok(message) => message,
|
|
||||||
Err(error) => {
|
|
||||||
log::warn!("error reading message: {:?}", error);
|
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
incoming_tx.unbounded_send(message).ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
mem::forget(project);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -655,7 +655,7 @@ async fn init_test(
|
||||||
(project, headless, fs)
|
(project, headless, fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_project(ssh: Arc<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
|
fn build_project(ssh: Model<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);
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
mod headless_project;
|
mod headless_project;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub mod unix;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod remote_editing_tests;
|
mod remote_editing_tests;
|
||||||
|
|
||||||
|
|
336
crates/remote_server/src/unix.rs
Normal file
336
crates/remote_server/src/unix.rs
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
use crate::HeadlessProject;
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use fs::RealFs;
|
||||||
|
use futures::channel::mpsc;
|
||||||
|
use futures::{select, select_biased, AsyncRead, AsyncWrite, FutureExt, SinkExt};
|
||||||
|
use gpui::{AppContext, Context as _};
|
||||||
|
use remote::ssh_session::ChannelClient;
|
||||||
|
use remote::{
|
||||||
|
json_log::LogRecord,
|
||||||
|
protocol::{read_message, write_message},
|
||||||
|
};
|
||||||
|
use rpc::proto::Envelope;
|
||||||
|
use smol::Async;
|
||||||
|
use smol::{io::AsyncWriteExt, net::unix::UnixListener, stream::StreamExt as _};
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
io::Write,
|
||||||
|
mem,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init_logging(log_file: Option<PathBuf>) -> Result<()> {
|
||||||
|
if let Some(log_file) = log_file {
|
||||||
|
let target = Box::new(if log_file.exists() {
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.append(true)
|
||||||
|
.open(&log_file)
|
||||||
|
.context("Failed to open log file in append mode")?
|
||||||
|
} else {
|
||||||
|
std::fs::File::create(&log_file).context("Failed to create log file")?
|
||||||
|
});
|
||||||
|
|
||||||
|
env_logger::Builder::from_default_env()
|
||||||
|
.target(env_logger::Target::Pipe(target))
|
||||||
|
.init();
|
||||||
|
} else {
|
||||||
|
env_logger::builder()
|
||||||
|
.format(|buf, record| {
|
||||||
|
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
|
||||||
|
buf.write_all(b"\n")?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_server(
|
||||||
|
stdin_listener: UnixListener,
|
||||||
|
stdout_listener: UnixListener,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Arc<ChannelClient> {
|
||||||
|
// This is the server idle timeout. If no connection comes in in this timeout, the server will shut down.
|
||||||
|
const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60);
|
||||||
|
|
||||||
|
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
let (app_quit_tx, mut app_quit_rx) = mpsc::unbounded::<()>();
|
||||||
|
|
||||||
|
cx.on_app_quit(move |_| {
|
||||||
|
let mut app_quit_tx = app_quit_tx.clone();
|
||||||
|
async move {
|
||||||
|
app_quit_tx.send(()).await.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
let mut stdin_incoming = stdin_listener.incoming();
|
||||||
|
let mut stdout_incoming = stdout_listener.incoming();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let streams = futures::future::join(stdin_incoming.next(), stdout_incoming.next());
|
||||||
|
|
||||||
|
log::info!("server: accepting new connections");
|
||||||
|
let result = select! {
|
||||||
|
streams = streams.fuse() => {
|
||||||
|
let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream))) = streams else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
anyhow::Ok((stdin_stream, stdout_stream))
|
||||||
|
}
|
||||||
|
_ = futures::FutureExt::fuse(smol::Timer::after(IDLE_TIMEOUT)) => {
|
||||||
|
log::warn!("server: timed out waiting for new connections after {:?}. exiting.", IDLE_TIMEOUT);
|
||||||
|
cx.update(|cx| {
|
||||||
|
// TODO: This is a hack, because in a headless project, shutdown isn't executed
|
||||||
|
// when calling quit, but it should be.
|
||||||
|
cx.shutdown();
|
||||||
|
cx.quit();
|
||||||
|
})?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = app_quit_rx.next().fuse() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok((mut stdin_stream, mut stdout_stream)) = result else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut input_buffer = Vec::new();
|
||||||
|
let mut output_buffer = Vec::new();
|
||||||
|
loop {
|
||||||
|
select_biased! {
|
||||||
|
_ = app_quit_rx.next().fuse() => {
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin_message = read_message(&mut stdin_stream, &mut input_buffer).fuse() => {
|
||||||
|
let message = match stdin_message {
|
||||||
|
Ok(message) => message,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("server: error reading message on stdin: {}. exiting.", error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(error) = incoming_tx.unbounded_send(message) {
|
||||||
|
log::error!("server: failed to send message to application: {:?}. exiting.", error);
|
||||||
|
return Err(anyhow!(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outgoing_message = outgoing_rx.next().fuse() => {
|
||||||
|
let Some(message) = outgoing_message else {
|
||||||
|
log::error!("server: stdout handler, no message");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) =
|
||||||
|
write_message(&mut stdout_stream, &mut output_buffer, message).await
|
||||||
|
{
|
||||||
|
log::error!("server: failed to write stdout message: {:?}", error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(error) = stdout_stream.flush().await {
|
||||||
|
log::error!("server: failed to flush stdout message: {:?}", error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
ChannelClient::new(incoming_rx, outgoing_tx, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_run(pid_file: PathBuf, stdin_socket: PathBuf, stdout_socket: PathBuf) -> Result<()> {
|
||||||
|
write_pid_file(&pid_file)
|
||||||
|
.with_context(|| format!("failed to write pid file: {:?}", &pid_file))?;
|
||||||
|
|
||||||
|
let stdin_listener = UnixListener::bind(stdin_socket).context("failed to bind stdin socket")?;
|
||||||
|
let stdout_listener =
|
||||||
|
UnixListener::bind(stdout_socket).context("failed to bind stdout socket")?;
|
||||||
|
|
||||||
|
gpui::App::headless().run(move |cx| {
|
||||||
|
settings::init(cx);
|
||||||
|
HeadlessProject::init(cx);
|
||||||
|
|
||||||
|
let session = start_server(stdin_listener, stdout_listener, cx);
|
||||||
|
let project = cx.new_model(|cx| {
|
||||||
|
HeadlessProject::new(session, Arc::new(RealFs::new(Default::default(), None)), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
mem::forget(project);
|
||||||
|
});
|
||||||
|
log::info!("server: gpui app is shut down. quitting.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_proxy(identifier: String) -> Result<()> {
|
||||||
|
log::debug!("proxy: starting up. PID: {}", std::process::id());
|
||||||
|
|
||||||
|
let project_dir = ensure_project_dir(&identifier)?;
|
||||||
|
|
||||||
|
let pid_file = project_dir.join("server.pid");
|
||||||
|
let stdin_socket = project_dir.join("stdin.sock");
|
||||||
|
let stdout_socket = project_dir.join("stdout.sock");
|
||||||
|
let log_file = project_dir.join("server.log");
|
||||||
|
|
||||||
|
let server_running = check_pid_file(&pid_file)?;
|
||||||
|
if !server_running {
|
||||||
|
spawn_server(&log_file, &pid_file, &stdin_socket, &stdout_socket)?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdin_task = smol::spawn(async move {
|
||||||
|
let stdin = Async::new(std::io::stdin())?;
|
||||||
|
let stream = smol::net::unix::UnixStream::connect(stdin_socket).await?;
|
||||||
|
handle_io(stdin, stream, "stdin").await
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout_task: smol::Task<Result<()>> = smol::spawn(async move {
|
||||||
|
let stdout = Async::new(std::io::stdout())?;
|
||||||
|
let stream = smol::net::unix::UnixStream::connect(stdout_socket).await?;
|
||||||
|
handle_io(stream, stdout, "stdout").await
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(forwarding_result) =
|
||||||
|
smol::block_on(async move { smol::future::race(stdin_task, stdout_task).await })
|
||||||
|
{
|
||||||
|
log::error!(
|
||||||
|
"proxy: failed to forward messages: {:?}, terminating...",
|
||||||
|
forwarding_result
|
||||||
|
);
|
||||||
|
return Err(forwarding_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_project_dir(identifier: &str) -> Result<PathBuf> {
|
||||||
|
let project_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
|
let project_dir = PathBuf::from(project_dir)
|
||||||
|
.join(".local")
|
||||||
|
.join("state")
|
||||||
|
.join("zed-remote-server")
|
||||||
|
.join(identifier);
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&project_dir)?;
|
||||||
|
|
||||||
|
Ok(project_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_server(
|
||||||
|
log_file: &Path,
|
||||||
|
pid_file: &Path,
|
||||||
|
stdin_socket: &Path,
|
||||||
|
stdout_socket: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
if stdin_socket.exists() {
|
||||||
|
std::fs::remove_file(&stdin_socket)?;
|
||||||
|
}
|
||||||
|
if stdout_socket.exists() {
|
||||||
|
std::fs::remove_file(&stdout_socket)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let binary_name = std::env::current_exe()?;
|
||||||
|
let server_process = std::process::Command::new(binary_name)
|
||||||
|
.arg("run")
|
||||||
|
.arg("--log-file")
|
||||||
|
.arg(log_file)
|
||||||
|
.arg("--pid-file")
|
||||||
|
.arg(pid_file)
|
||||||
|
.arg("--stdin-socket")
|
||||||
|
.arg(stdin_socket)
|
||||||
|
.arg("--stdout-socket")
|
||||||
|
.arg(stdout_socket)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
log::debug!("proxy: server started. PID: {:?}", server_process.id());
|
||||||
|
|
||||||
|
let mut total_time_waited = std::time::Duration::from_secs(0);
|
||||||
|
let wait_duration = std::time::Duration::from_millis(20);
|
||||||
|
while !stdout_socket.exists() || !stdin_socket.exists() {
|
||||||
|
log::debug!("proxy: waiting for server to be ready to accept connections...");
|
||||||
|
std::thread::sleep(wait_duration);
|
||||||
|
total_time_waited += wait_duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"proxy: server ready to accept connections. total time waited: {:?}",
|
||||||
|
total_time_waited
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_pid_file(path: &Path) -> Result<bool> {
|
||||||
|
let Some(pid) = std::fs::read_to_string(&path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|contents| contents.parse::<u32>().ok())
|
||||||
|
else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("proxy: Checking if process with PID {} exists...", pid);
|
||||||
|
match std::process::Command::new("kill")
|
||||||
|
.arg("-0")
|
||||||
|
.arg(pid.to_string())
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
log::debug!("proxy: Process with PID {} exists. NOT spawning new server, but attaching to existing one.", pid);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!("proxy: Found PID file, but process with that PID does not exist. Removing PID file.");
|
||||||
|
std::fs::remove_file(&path).context("proxy: Failed to remove PID file")?;
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_pid_file(path: &Path) -> Result<()> {
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_file(path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(path, std::process::id().to_string()).context("Failed to write PID file")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_io<R, W>(mut reader: R, mut writer: W, socket_name: &str) -> Result<()>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin,
|
||||||
|
W: AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
use remote::protocol::read_message_raw;
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
loop {
|
||||||
|
read_message_raw(&mut reader, &mut buffer)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("proxy: failed to read message from {}", socket_name))?;
|
||||||
|
|
||||||
|
write_size_prefixed_buffer(&mut writer, &mut buffer)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("proxy: 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(())
|
||||||
|
}
|
|
@ -265,7 +265,7 @@ impl TitleBar {
|
||||||
fn render_ssh_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
fn render_ssh_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||||
let host = self.project.read(cx).ssh_connection_string(cx)?;
|
let host = self.project.read(cx).ssh_connection_string(cx)?;
|
||||||
let meta = SharedString::from(format!("Connected to: {host}"));
|
let meta = SharedString::from(format!("Connected to: {host}"));
|
||||||
let indicator_color = if self.project.read(cx).ssh_is_connected()? {
|
let indicator_color = if self.project.read(cx).ssh_is_connected(cx)? {
|
||||||
Color::Success
|
Color::Success
|
||||||
} else {
|
} else {
|
||||||
Color::Warning
|
Color::Warning
|
||||||
|
|
|
@ -51,6 +51,7 @@ postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
|
release_channel.workspace = true
|
||||||
remote.workspace = true
|
remote.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -61,7 +61,8 @@ 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, SshRemoteClient};
|
use release_channel::ReleaseChannel;
|
||||||
|
use remote::{SshClientDelegate, SshConnectionOptions};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use session::AppSession;
|
use session::AppSession;
|
||||||
use settings::{InvalidSettingsError, Settings};
|
use settings::{InvalidSettingsError, Settings};
|
||||||
|
@ -5514,22 +5515,31 @@ 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<SshRemoteClient>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
paths: Vec<PathBuf>,
|
paths: Vec<PathBuf>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
|
let release_channel = ReleaseChannel::global(cx);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let serialized_ssh_project = persistence::DB
|
let (serialized_ssh_project, workspace_id, serialized_workspace) =
|
||||||
.get_or_create_ssh_project(
|
serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
|
||||||
connection_options.host.clone(),
|
|
||||||
connection_options.port,
|
let identifier_prefix = match release_channel {
|
||||||
paths
|
ReleaseChannel::Stable => None,
|
||||||
.iter()
|
_ => Some(format!("{}-", release_channel.dev_name())),
|
||||||
.map(|path| path.to_string_lossy().to_string())
|
};
|
||||||
.collect::<Vec<_>>(),
|
let unique_identifier = format!(
|
||||||
connection_options.username.clone(),
|
"{}workspace-{}",
|
||||||
)
|
identifier_prefix.unwrap_or_default(),
|
||||||
|
workspace_id.0
|
||||||
|
);
|
||||||
|
|
||||||
|
let session = cx
|
||||||
|
.update(|cx| {
|
||||||
|
remote::SshRemoteClient::new(unique_identifier, connection_options, delegate, cx)
|
||||||
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let project = cx.update(|cx| {
|
let project = cx.update(|cx| {
|
||||||
|
@ -5561,17 +5571,6 @@ pub fn open_ssh_project(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let serialized_workspace =
|
|
||||||
persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
|
|
||||||
|
|
||||||
let workspace_id = if let Some(workspace_id) =
|
|
||||||
serialized_workspace.as_ref().map(|workspace| workspace.id)
|
|
||||||
{
|
|
||||||
workspace_id
|
|
||||||
} else {
|
|
||||||
persistence::DB.next_id().await?
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.update_window(window.into(), |_, cx| {
|
cx.update_window(window.into(), |_, cx| {
|
||||||
cx.replace_root_view(|cx| {
|
cx.replace_root_view(|cx| {
|
||||||
let mut workspace =
|
let mut workspace =
|
||||||
|
@ -5603,6 +5602,45 @@ pub fn open_ssh_project(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_ssh_project(
|
||||||
|
connection_options: SshConnectionOptions,
|
||||||
|
paths: Vec<PathBuf>,
|
||||||
|
cx: &AsyncAppContext,
|
||||||
|
) -> Task<
|
||||||
|
Result<(
|
||||||
|
SerializedSshProject,
|
||||||
|
WorkspaceId,
|
||||||
|
Option<SerializedWorkspace>,
|
||||||
|
)>,
|
||||||
|
> {
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let serialized_ssh_project = persistence::DB
|
||||||
|
.get_or_create_ssh_project(
|
||||||
|
connection_options.host.clone(),
|
||||||
|
connection_options.port,
|
||||||
|
paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
connection_options.username.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let serialized_workspace =
|
||||||
|
persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
|
||||||
|
|
||||||
|
let workspace_id = if let Some(workspace_id) =
|
||||||
|
serialized_workspace.as_ref().map(|workspace| workspace.id)
|
||||||
|
{
|
||||||
|
workspace_id
|
||||||
|
} else {
|
||||||
|
persistence::DB.next_id().await?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((serialized_ssh_project, workspace_id, serialized_workspace))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn join_dev_server_project(
|
pub fn join_dev_server_project(
|
||||||
dev_server_project_id: DevServerProjectId,
|
dev_server_project_id: DevServerProjectId,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue