Clean up handling of serialized ssh connection ids (#36781)

Small follow-up to #36714

Release Notes:

- N/A
This commit is contained in:
Max Brunsfeld 2025-08-22 15:44:58 -07:00 committed by GitHub
parent bc566fe18e
commit 153724aad3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 97 deletions

View file

@ -52,11 +52,6 @@ use util::{
paths::{PathStyle, RemotePathBuf}, paths::{PathStyle, RemotePathBuf},
}; };
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
pub struct SshProjectId(pub u64);
#[derive(Clone)] #[derive(Clone)]
pub struct SshSocket { pub struct SshSocket {
connection_options: SshConnectionOptions, connection_options: SshConnectionOptions,

View file

@ -9,13 +9,13 @@ use std::{
}; };
use anyhow::{Context as _, Result, bail}; use anyhow::{Context as _, Result, bail};
use collections::HashMap;
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
use language::{LanguageName, Toolchain}; use language::{LanguageName, Toolchain};
use project::WorktreeId; use project::WorktreeId;
use remote::ssh_session::SshProjectId;
use sqlez::{ use sqlez::{
bindable::{Bind, Column, StaticColumnCount}, bindable::{Bind, Column, StaticColumnCount},
statement::{SqlType, Statement}, statement::{SqlType, Statement},
@ -33,7 +33,7 @@ use crate::{
use model::{ use model::{
GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
SerializedSshConnection, SerializedWorkspace, SerializedSshConnection, SerializedWorkspace, SshConnectionId,
}; };
use self::model::{DockStructure, SerializedWorkspaceLocation}; use self::model::{DockStructure, SerializedWorkspaceLocation};
@ -615,7 +615,7 @@ impl WorkspaceDb {
pub(crate) fn ssh_workspace_for_roots<P: AsRef<Path>>( pub(crate) fn ssh_workspace_for_roots<P: AsRef<Path>>(
&self, &self,
worktree_roots: &[P], worktree_roots: &[P],
ssh_project_id: SshProjectId, ssh_project_id: SshConnectionId,
) -> Option<SerializedWorkspace> { ) -> Option<SerializedWorkspace> {
self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id))
} }
@ -623,7 +623,7 @@ impl WorkspaceDb {
pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>( pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
&self, &self,
worktree_roots: &[P], worktree_roots: &[P],
ssh_connection_id: Option<SshProjectId>, ssh_connection_id: Option<SshConnectionId>,
) -> Option<SerializedWorkspace> { ) -> Option<SerializedWorkspace> {
// paths are sorted before db interactions to ensure that the order of the paths // paths are sorted before db interactions to ensure that the order of the paths
// doesn't affect the workspace selection for existing workspaces // doesn't affect the workspace selection for existing workspaces
@ -762,15 +762,21 @@ impl WorkspaceDb {
/// that used this workspace previously /// that used this workspace previously
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
let paths = workspace.paths.serialize(); let paths = workspace.paths.serialize();
let ssh_connection_id = match &workspace.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Ssh(serialized_ssh_connection) => {
Some(serialized_ssh_connection.id.0)
}
};
log::debug!("Saving workspace at location: {:?}", workspace.location); log::debug!("Saving workspace at location: {:?}", workspace.location);
self.write(move |conn| { self.write(move |conn| {
conn.with_savepoint("update_worktrees", || { conn.with_savepoint("update_worktrees", || {
let ssh_connection_id = match &workspace.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Ssh(connection) => {
Some(Self::get_or_create_ssh_connection_query(
conn,
connection.host.clone(),
connection.port,
connection.user.clone(),
)?.0)
}
};
// Clear out panes and pane_groups // Clear out panes and pane_groups
conn.exec_bound(sql!( conn.exec_bound(sql!(
DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM pane_groups WHERE workspace_id = ?1;
@ -893,39 +899,34 @@ impl WorkspaceDb {
host: String, host: String,
port: Option<u16>, port: Option<u16>,
user: Option<String>, user: Option<String>,
) -> Result<SshProjectId> { ) -> Result<SshConnectionId> {
if let Some(id) = self self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user))
.get_ssh_connection(host.clone(), port, user.clone()) .await
.await? }
fn get_or_create_ssh_connection_query(
this: &Connection,
host: String,
port: Option<u16>,
user: Option<String>,
) -> Result<SshConnectionId> {
if let Some(id) = this.select_row_bound(sql!(
SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1
))?((host.clone(), port, user.clone()))?
{ {
Ok(SshProjectId(id)) Ok(SshConnectionId(id))
} else { } else {
log::debug!("Inserting SSH project at host {host}"); log::debug!("Inserting SSH project at host {host}");
let id = self let id = this.select_row_bound(sql!(
.insert_ssh_connection(host, port, user) INSERT INTO ssh_connections (
.await? host,
.context("failed to insert ssh project")?; port,
Ok(SshProjectId(id)) user
} ) VALUES (?1, ?2, ?3)
} RETURNING id
))?((host, port, user))?
query! { .context("failed to insert ssh project")?;
async fn get_ssh_connection(host: String, port: Option<u16>, user: Option<String>) -> Result<Option<u64>> { Ok(SshConnectionId(id))
SELECT id
FROM ssh_connections
WHERE host IS ? AND port IS ? AND user IS ?
LIMIT 1
}
}
query! {
async fn insert_ssh_connection(host: String, port: Option<u16>, user: Option<String>) -> Result<Option<u64>> {
INSERT INTO ssh_connections (
host,
port,
user
) VALUES (?1, ?2, ?3)
RETURNING id
} }
} }
@ -963,7 +964,7 @@ impl WorkspaceDb {
fn session_workspaces( fn session_workspaces(
&self, &self,
session_id: String, session_id: String,
) -> Result<Vec<(PathList, Option<u64>, Option<SshProjectId>)>> { ) -> Result<Vec<(PathList, Option<u64>, Option<SshConnectionId>)>> {
Ok(self Ok(self
.session_workspaces_query(session_id)? .session_workspaces_query(session_id)?
.into_iter() .into_iter()
@ -971,7 +972,7 @@ impl WorkspaceDb {
( (
PathList::deserialize(&SerializedPathList { paths, order }), PathList::deserialize(&SerializedPathList { paths, order }),
window_id, window_id,
ssh_connection_id.map(SshProjectId), ssh_connection_id.map(SshConnectionId),
) )
}) })
.collect()) .collect())
@ -1001,15 +1002,15 @@ impl WorkspaceDb {
} }
} }
fn ssh_connections(&self) -> Result<Vec<SerializedSshConnection>> { fn ssh_connections(&self) -> Result<HashMap<SshConnectionId, SerializedSshConnection>> {
Ok(self Ok(self
.ssh_connections_query()? .ssh_connections_query()?
.into_iter() .into_iter()
.map(|(id, host, port, user)| SerializedSshConnection { .map(|(id, host, port, user)| {
id: SshProjectId(id), (
host, SshConnectionId(id),
port, SerializedSshConnection { host, port, user },
user, )
}) })
.collect()) .collect())
} }
@ -1021,19 +1022,18 @@ impl WorkspaceDb {
} }
} }
pub fn ssh_connection(&self, id: SshProjectId) -> Result<SerializedSshConnection> { pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result<SerializedSshConnection> {
let row = self.ssh_connection_query(id.0)?; let row = self.ssh_connection_query(id.0)?;
Ok(SerializedSshConnection { Ok(SerializedSshConnection {
id: SshProjectId(row.0), host: row.0,
host: row.1, port: row.1,
port: row.2, user: row.2,
user: row.3,
}) })
} }
query! { query! {
fn ssh_connection_query(id: u64) -> Result<(u64, String, Option<u16>, Option<String>)> { fn ssh_connection_query(id: u64) -> Result<(String, Option<u16>, Option<String>)> {
SELECT id, host, port, user SELECT host, port, user
FROM ssh_connections FROM ssh_connections
WHERE id = ? WHERE id = ?
} }
@ -1075,10 +1075,8 @@ impl WorkspaceDb {
let ssh_connections = self.ssh_connections()?; let ssh_connections = self.ssh_connections()?;
for (id, paths, ssh_connection_id) in self.recent_workspaces()? { for (id, paths, ssh_connection_id) in self.recent_workspaces()? {
if let Some(ssh_connection_id) = ssh_connection_id.map(SshProjectId) { if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) {
if let Some(ssh_connection) = if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) {
ssh_connections.iter().find(|rp| rp.id == ssh_connection_id)
{
result.push(( result.push((
id, id,
SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), SerializedWorkspaceLocation::Ssh(ssh_connection.clone()),
@ -2340,12 +2338,10 @@ mod tests {
] ]
.into_iter() .into_iter()
.map(|(host, user)| async { .map(|(host, user)| async {
let id = db db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string()))
.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string()))
.await .await
.unwrap(); .unwrap();
SerializedSshConnection { SerializedSshConnection {
id,
host: host.into(), host: host.into(),
port: None, port: None,
user: Some(user.into()), user: Some(user.into()),
@ -2501,26 +2497,34 @@ mod tests {
let stored_projects = db.ssh_connections().unwrap(); let stored_projects = db.ssh_connections().unwrap();
assert_eq!( assert_eq!(
stored_projects, stored_projects,
&[ [
SerializedSshConnection { (
id: ids[0], ids[0],
host: "example.com".into(), SerializedSshConnection {
port: None, host: "example.com".into(),
user: None, port: None,
}, user: None,
SerializedSshConnection { }
id: ids[1], ),
host: "anotherexample.com".into(), (
port: Some(123), ids[1],
user: Some("user2".into()), SerializedSshConnection {
}, host: "anotherexample.com".into(),
SerializedSshConnection { port: Some(123),
id: ids[2], user: Some("user2".into()),
host: "yetanother.com".into(), }
port: Some(345), ),
user: None, (
}, ids[2],
SerializedSshConnection {
host: "yetanother.com".into(),
port: Some(345),
user: None,
}
),
] ]
.into_iter()
.collect::<HashMap<_, _>>(),
); );
} }

View file

@ -12,7 +12,6 @@ use db::sqlez::{
use gpui::{AsyncWindowContext, Entity, WeakEntity}; use gpui::{AsyncWindowContext, Entity, WeakEntity};
use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
use remote::ssh_session::SshProjectId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
@ -22,9 +21,13 @@ use std::{
use util::ResultExt; use util::ResultExt;
use uuid::Uuid; use uuid::Uuid;
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
)]
pub(crate) struct SshConnectionId(pub u64);
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct SerializedSshConnection { pub struct SerializedSshConnection {
pub id: SshProjectId,
pub host: String, pub host: String,
pub port: Option<u16>, pub port: Option<u16>,
pub user: Option<String>, pub user: Option<String>,

View file

@ -74,10 +74,7 @@ use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
}; };
use remote::{ use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
SshClientDelegate, SshConnectionOptions,
ssh_session::{ConnectionIdentifier, SshProjectId},
};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use session::AppSession; use session::AppSession;
@ -1128,7 +1125,6 @@ pub struct Workspace {
terminal_provider: Option<Box<dyn TerminalProvider>>, terminal_provider: Option<Box<dyn TerminalProvider>>,
debugger_provider: Option<Arc<dyn DebuggerProvider>>, debugger_provider: Option<Arc<dyn DebuggerProvider>>,
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>, serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
serialized_ssh_connection_id: Option<SshProjectId>,
_items_serializer: Task<Result<()>>, _items_serializer: Task<Result<()>>,
session_id: Option<String>, session_id: Option<String>,
scheduled_tasks: Vec<Task<()>>, scheduled_tasks: Vec<Task<()>>,
@ -1461,7 +1457,7 @@ impl Workspace {
serializable_items_tx, serializable_items_tx,
_items_serializer, _items_serializer,
session_id: Some(session_id), session_id: Some(session_id),
serialized_ssh_connection_id: None,
scheduled_tasks: Vec::new(), scheduled_tasks: Vec::new(),
} }
} }
@ -5288,11 +5284,9 @@ impl Workspace {
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
let paths = PathList::new(&self.root_paths(cx)); let paths = PathList::new(&self.root_paths(cx));
let connection = self.project.read(cx).ssh_connection_options(cx); if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) {
if let Some((id, connection)) = self.serialized_ssh_connection_id.zip(connection) {
WorkspaceLocation::Location( WorkspaceLocation::Location(
SerializedWorkspaceLocation::Ssh(SerializedSshConnection { SerializedWorkspaceLocation::Ssh(SerializedSshConnection {
id,
host: connection.host, host: connection.host,
port: connection.port, port: connection.port,
user: connection.username, user: connection.username,