remote projects per user (#10594)

Release Notes:

- Made remote projects per-user instead of per-channel. If you'd like to
be part of the remote development alpha, please email hi@zed.dev.

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet <bennetbo@gmx.de>
Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
Conrad Irwin 2024-04-23 15:33:09 -06:00 committed by GitHub
parent 8ae4c3277f
commit e0c83a1d32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2807 additions and 1625 deletions

View file

@ -655,8 +655,6 @@ pub struct ChannelsForUser {
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub hosted_projects: Vec<proto::HostedProject>,
pub dev_servers: Vec<dev_server::Model>,
pub remote_projects: Vec<proto::RemoteProject>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
@ -764,6 +762,7 @@ pub struct Project {
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
pub remote_project_id: Option<RemoteProjectId>,
}
pub struct ProjectCollaborator {
@ -786,8 +785,7 @@ impl ProjectCollaborator {
#[derive(Debug)]
pub struct LeftProject {
pub id: ProjectId,
pub host_user_id: Option<UserId>,
pub host_connection_id: Option<ConnectionId>,
pub should_unshare: bool,
pub connection_ids: Vec<ConnectionId>,
}

View file

@ -640,15 +640,10 @@ impl Database {
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
.await?;
let dev_servers = self.get_dev_servers(&channel_ids, tx).await?;
let remote_projects = self.get_remote_projects(&channel_ids, tx).await?;
Ok(ChannelsForUser {
channel_memberships,
channels,
hosted_projects,
dev_servers,
remote_projects,
channel_participants,
latest_buffer_versions,
latest_channel_messages,

View file

@ -1,6 +1,9 @@
use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter};
use rpc::proto;
use sea_orm::{
ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter,
};
use super::{channel, dev_server, ChannelId, Database, DevServerId, UserId};
use super::{dev_server, remote_project, Database, DevServerId, UserId};
impl Database {
pub async fn get_dev_server(
@ -16,40 +19,105 @@ impl Database {
.await
}
pub async fn get_dev_servers(
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
self.transaction(|tx| async move {
Ok(dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.all(&*tx)
.await?)
})
.await
}
pub async fn remote_projects_update(
&self,
channel_ids: &Vec<ChannelId>,
user_id: UserId,
) -> crate::Result<proto::RemoteProjectsUpdate> {
self.transaction(
|tx| async move { self.remote_projects_update_internal(user_id, &tx).await },
)
.await
}
pub async fn remote_projects_update_internal(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<dev_server::Model>> {
let servers = dev_server::Entity::find()
.filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
) -> crate::Result<proto::RemoteProjectsUpdate> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.all(tx)
.await?;
Ok(servers)
let remote_projects = remote_project::Entity::find()
.filter(
remote_project::Column::DevServerId
.is_in(dev_servers.iter().map(|d| d.id).collect::<Vec<_>>()),
)
.find_also_related(super::project::Entity)
.all(tx)
.await?;
Ok(proto::RemoteProjectsUpdate {
dev_servers: dev_servers
.into_iter()
.map(|d| d.to_proto(proto::DevServerStatus::Offline))
.collect(),
remote_projects: remote_projects
.into_iter()
.map(|(remote_project, project)| remote_project.to_proto(project))
.collect(),
})
}
pub async fn create_dev_server(
&self,
channel_id: ChannelId,
name: &str,
hashed_access_token: &str,
user_id: UserId,
) -> crate::Result<(channel::Model, dev_server::Model)> {
) -> crate::Result<(dev_server::Model, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_admin(&channel, user_id, &tx)
.await?;
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
id: ActiveValue::NotSet,
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
channel_id: ActiveValue::Set(channel_id),
name: ActiveValue::Set(name.to_string()),
user_id: ActiveValue::Set(user_id),
})
.exec_with_returning(&*tx)
.await?;
Ok((channel, dev_server))
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
Ok((dev_server, remote_projects))
})
.await
}
pub async fn delete_dev_server(
&self,
id: DevServerId,
user_id: UserId,
) -> crate::Result<proto::RemoteProjectsUpdate> {
self.transaction(|tx| async move {
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
};
if dev_server.user_id != user_id {
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
}
remote_project::Entity::delete_many()
.filter(remote_project::Column::DevServerId.eq(id))
.exec(&*tx)
.await?;
dev_server::Entity::delete(dev_server.into_active_model())
.exec(&*tx)
.await?;
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
Ok(remote_projects)
})
.await
}

View file

@ -30,6 +30,7 @@ impl Database {
room_id: RoomId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
remote_project_id: Option<RemoteProjectId>,
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
@ -58,6 +59,30 @@ impl Database {
return Err(anyhow!("guests cannot share projects"))?;
}
if let Some(remote_project_id) = remote_project_id {
let project = project::Entity::find()
.filter(project::Column::RemoteProjectId.eq(Some(remote_project_id)))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project"))?;
if project.room_id.is_some() {
return Err(anyhow!("project already shared"))?;
};
let project = project::Entity::update(project::ActiveModel {
room_id: ActiveValue::Set(Some(room_id)),
..project.into_active_model()
})
.exec(&*tx)
.await?;
// todo! check user is a project-collaborator
let room = self.get_room(room_id, &tx).await?;
return Ok((project.id, room));
}
let project = project::ActiveModel {
room_id: ActiveValue::set(Some(participant.room_id)),
host_user_id: ActiveValue::set(Some(participant.user_id)),
@ -111,6 +136,7 @@ impl Database {
&self,
project_id: ProjectId,
connection: ConnectionId,
user_id: Option<UserId>,
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
@ -118,19 +144,37 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project not found"))?;
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
if project.host_connection()? == connection {
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
Ok((room, guest_connection_ids))
} else {
Err(anyhow!("cannot unshare a project hosted by another user"))?
return Ok((room, guest_connection_ids));
}
if let Some(remote_project_id) = project.remote_project_id {
if let Some(user_id) = user_id {
if user_id
!= self
.owner_for_remote_project(remote_project_id, &tx)
.await?
{
Err(anyhow!("cannot unshare a project hosted by another user"))?
}
project::Entity::update(project::ActiveModel {
room_id: ActiveValue::Set(None),
..project.into_active_model()
})
.exec(&*tx)
.await?;
return Ok((room, guest_connection_ids));
}
}
Err(anyhow!("cannot unshare a project hosted by another user"))?
})
.await
}
@ -753,6 +797,7 @@ impl Database {
name: language_server.name,
})
.collect(),
remote_project_id: project.remote_project_id,
};
Ok((project, replica_id as ReplicaId))
}
@ -794,8 +839,7 @@ impl Database {
Ok(LeftProject {
id: project.id,
connection_ids,
host_user_id: None,
host_connection_id: None,
should_unshare: false,
})
})
.await
@ -832,7 +876,7 @@ impl Database {
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let connection_ids = collaborators
let connection_ids: Vec<ConnectionId> = collaborators
.into_iter()
.map(|collaborator| collaborator.connection())
.collect();
@ -870,8 +914,7 @@ impl Database {
let left_project = LeftProject {
id: project_id,
host_user_id: project.host_user_id,
host_connection_id: Some(project.host_connection()?),
should_unshare: connection == project.host_connection()?,
connection_ids,
};
Ok((room, left_project))
@ -914,7 +957,7 @@ impl Database {
capability: Capability,
tx: &DatabaseTransaction,
) -> Result<(project::Model, ChannelRole)> {
let (project, remote_project) = project::Entity::find_by_id(project_id)
let (mut project, remote_project) = project::Entity::find_by_id(project_id)
.find_also_related(remote_project::Entity)
.one(tx)
.await?
@ -933,27 +976,44 @@ impl Database {
PrincipalId::UserId(user_id) => user_id,
};
let role = if let Some(remote_project) = remote_project {
let channel = channel::Entity::find_by_id(remote_project.channel_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
self.check_user_is_channel_participant(&channel, user_id, &tx)
.await?
} else if let Some(room_id) = project.room_id {
// what's the users role?
let current_participant = room_participant::Entity::find()
let role_from_room = if let Some(room_id) = project.room_id {
room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
current_participant.role.unwrap_or(ChannelRole::Guest)
.and_then(|participant| participant.role)
} else {
return Err(anyhow!("not authorized to read projects"))?;
None
};
let role_from_remote_project = if let Some(remote_project) = remote_project {
let dev_server = dev_server::Entity::find_by_id(remote_project.dev_server_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
if user_id == dev_server.user_id {
// If the user left the room "uncleanly" they may rejoin the
// remote project before leave_room runs. IN that case kick
// the project out of the room pre-emptively.
if role_from_room.is_none() {
project = project::Entity::update(project::ActiveModel {
room_id: ActiveValue::Set(None),
..project.into_active_model()
})
.exec(tx)
.await?;
}
Some(ChannelRole::Admin)
} else {
None
}
} else {
None
};
let role = role_from_remote_project
.or(role_from_room)
.unwrap_or(ChannelRole::Banned);
match capability {
Capability::ReadWrite => {

View file

@ -8,8 +8,8 @@ use sea_orm::{
use crate::db::ProjectId;
use super::{
channel, project, project_collaborator, remote_project, worktree, ChannelId, Database,
DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId,
RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
};
impl Database {
@ -26,29 +26,6 @@ impl Database {
.await
}
pub async fn get_remote_projects(
&self,
channel_ids: &Vec<ChannelId>,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<proto::RemoteProject>> {
let servers = remote_project::Entity::find()
.filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
.find_also_related(project::Entity)
.all(tx)
.await?;
Ok(servers
.into_iter()
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: remote_project.channel_id.to_proto(),
name: remote_project.name,
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
.collect())
}
pub async fn get_remote_projects_for_dev_server(
&self,
dev_server_id: DevServerId,
@ -64,8 +41,6 @@ impl Database {
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: remote_project.channel_id.to_proto(),
name: remote_project.name,
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
@ -74,6 +49,38 @@ impl Database {
.await
}
pub async fn remote_project_ids_for_user(
&self,
user_id: UserId,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<RemoteProjectId>> {
let dev_servers = dev_server::Entity::find()
.filter(dev_server::Column::UserId.eq(user_id))
.find_with_related(remote_project::Entity)
.all(tx)
.await?;
Ok(dev_servers
.into_iter()
.flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
.collect())
}
pub async fn owner_for_remote_project(
&self,
remote_project_id: RemoteProjectId,
tx: &DatabaseTransaction,
) -> crate::Result<UserId> {
let dev_server = remote_project::Entity::find_by_id(remote_project_id)
.find_also_related(dev_server::Entity)
.one(tx)
.await?
.and_then(|(_, dev_server)| dev_server)
.ok_or_else(|| anyhow!("no remote project"))?;
Ok(dev_server.user_id)
}
pub async fn get_stale_dev_server_projects(
&self,
connection: ConnectionId,
@ -95,28 +102,30 @@ impl Database {
pub async fn create_remote_project(
&self,
channel_id: ChannelId,
dev_server_id: DevServerId,
name: &str,
path: &str,
user_id: UserId,
) -> crate::Result<(channel::Model, remote_project::Model)> {
) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_admin(&channel, user_id, &tx)
.await?;
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
if dev_server.user_id != user_id {
return Err(anyhow!("not your dev server"))?;
}
let project = remote_project::Entity::insert(remote_project::ActiveModel {
name: ActiveValue::Set(name.to_string()),
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
dev_server_id: ActiveValue::Set(dev_server_id),
path: ActiveValue::Set(path.to_string()),
})
.exec_with_returning(&*tx)
.await?;
Ok((channel, project))
let status = self.remote_projects_update_internal(user_id, &tx).await?;
Ok((project, status))
})
.await
}
@ -127,8 +136,13 @@ impl Database {
dev_server_id: DevServerId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> crate::Result<proto::RemoteProject> {
) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> {
self.transaction(|tx| async move {
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
.one(&*tx)
.await?
@ -168,7 +182,15 @@ impl Database {
.await?;
}
Ok(remote_project.to_proto(Some(project)))
let status = self
.remote_projects_update_internal(dev_server.user_id, &tx)
.await?;
Ok((
remote_project.to_proto(Some(project)),
dev_server.user_id,
status,
))
})
.await
}

View file

@ -849,11 +849,32 @@ impl Database {
.into_values::<_, QueryProjectIds>()
.all(&*tx)
.await?;
// if any project in the room has a remote-project-id that belongs to a dev server that this user owns.
let remote_projects_for_user = self
.remote_project_ids_for_user(leaving_participant.user_id, &tx)
.await?;
let remote_projects_to_unshare = project::Entity::find()
.filter(
Condition::all()
.add(project::Column::RoomId.eq(room_id))
.add(
project::Column::RemoteProjectId
.is_in(remote_projects_for_user.clone()),
),
)
.all(&*tx)
.await?
.into_iter()
.map(|project| project.id)
.collect::<HashSet<_>>();
let mut left_projects = HashMap::default();
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
.stream(&*tx)
.await?;
while let Some(collaborator) = collaborators.next().await {
let collaborator = collaborator?;
let left_project =
@ -861,9 +882,8 @@ impl Database {
.entry(collaborator.project_id)
.or_insert(LeftProject {
id: collaborator.project_id,
host_user_id: Default::default(),
connection_ids: Default::default(),
host_connection_id: None,
should_unshare: false,
});
let collaborator_connection_id = collaborator.connection();
@ -871,9 +891,10 @@ impl Database {
left_project.connection_ids.push(collaborator_connection_id);
}
if collaborator.is_host {
left_project.host_user_id = Some(collaborator.user_id);
left_project.host_connection_id = Some(collaborator_connection_id);
if (collaborator.is_host && collaborator.connection() == connection)
|| remote_projects_to_unshare.contains(&collaborator.project_id)
{
left_project.should_unshare = true;
}
}
drop(collaborators);
@ -915,6 +936,17 @@ impl Database {
.exec(&*tx)
.await?;
if !remote_projects_to_unshare.is_empty() {
project::Entity::update_many()
.filter(project::Column::Id.is_in(remote_projects_to_unshare))
.set(project::ActiveModel {
room_id: ActiveValue::Set(None),
..Default::default()
})
.exec(&*tx)
.await?;
}
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
let deleted = if room.participants.is_empty() {
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
@ -1264,38 +1296,46 @@ impl Database {
}
drop(db_participants);
let mut db_projects = db_room
let db_projects = db_room
.find_related(project::Entity)
.find_with_related(worktree::Entity)
.stream(tx)
.all(tx)
.await?;
while let Some(row) = db_projects.next().await {
let (db_project, db_worktree) = row?;
for (db_project, db_worktrees) in db_projects {
let host_connection = db_project.host_connection()?;
if let Some(participant) = participants.get_mut(&host_connection) {
let project = if let Some(project) = participant
.projects
.iter_mut()
.find(|project| project.id == db_project.id.to_proto())
{
project
} else {
participant.projects.push(proto::ParticipantProject {
id: db_project.id.to_proto(),
worktree_root_names: Default::default(),
});
participant.projects.last_mut().unwrap()
};
participant.projects.push(proto::ParticipantProject {
id: db_project.id.to_proto(),
worktree_root_names: Default::default(),
});
let project = participant.projects.last_mut().unwrap();
if let Some(db_worktree) = db_worktree {
for db_worktree in db_worktrees {
if db_worktree.visible {
project.worktree_root_names.push(db_worktree.root_name);
}
}
} else if let Some(remote_project_id) = db_project.remote_project_id {
let host = self.owner_for_remote_project(remote_project_id, tx).await?;
if let Some((_, participant)) = participants
.iter_mut()
.find(|(_, v)| v.user_id == host.to_proto())
{
participant.projects.push(proto::ParticipantProject {
id: db_project.id.to_proto(),
worktree_root_names: Default::default(),
});
let project = participant.projects.last_mut().unwrap();
for db_worktree in db_worktrees {
if db_worktree.visible {
project.worktree_root_names.push(db_worktree.root_name);
}
}
}
}
}
drop(db_projects);
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
let mut followers = Vec::new();

View file

@ -1,4 +1,4 @@
use crate::db::{ChannelId, DevServerId};
use crate::db::{DevServerId, UserId};
use rpc::proto;
use sea_orm::entity::prelude::*;
@ -8,20 +8,28 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: DevServerId,
pub name: String,
pub channel_id: ChannelId,
pub user_id: UserId,
pub hashed_token: String,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
pub enum Relation {
#[sea_orm(has_many = "super::remote_project::Entity")]
RemoteProject,
}
impl Related<super::remote_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::RemoteProject.def()
}
}
impl Model {
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
proto::DevServer {
dev_server_id: self.id.to_proto(),
channel_id: self.channel_id.to_proto(),
name: self.name.clone(),
status: status as i32,
}

View file

@ -1,5 +1,5 @@
use super::project;
use crate::db::{ChannelId, DevServerId, RemoteProjectId};
use crate::db::{DevServerId, RemoteProjectId};
use rpc::proto;
use sea_orm::entity::prelude::*;
@ -8,9 +8,7 @@ use sea_orm::entity::prelude::*;
pub struct Model {
#[sea_orm(primary_key)]
pub id: RemoteProjectId,
pub channel_id: ChannelId,
pub dev_server_id: DevServerId,
pub name: String,
pub path: String,
}
@ -20,6 +18,12 @@ impl ActiveModelBehavior for ActiveModel {}
pub enum Relation {
#[sea_orm(has_one = "super::project::Entity")]
Project,
#[sea_orm(
belongs_to = "super::dev_server::Entity",
from = "Column::DevServerId",
to = "super::dev_server::Column::Id"
)]
DevServer,
}
impl Related<super::project::Entity> for Entity {
@ -28,14 +32,18 @@ impl Related<super::project::Entity> for Entity {
}
}
impl Related<super::dev_server::Entity> for Entity {
fn to() -> RelationDef {
Relation::DevServer.def()
}
}
impl Model {
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
proto::RemoteProject {
id: self.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: self.channel_id.to_proto(),
dev_server_id: self.dev_server_id.to_proto(),
name: self.name.clone(),
path: self.path.clone(),
}
}

View file

@ -535,18 +535,18 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
// Projects shared by admins aren't counted.
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], None)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);

View file

@ -255,6 +255,13 @@ impl DevServerSession {
pub fn dev_server_id(&self) -> DevServerId {
self.0.dev_server_id().unwrap()
}
fn dev_server(&self) -> &dev_server::Model {
match &self.0.principal {
Principal::DevServer(dev_server) => dev_server,
_ => unreachable!(),
}
}
}
impl Deref for DevServerSession {
@ -405,6 +412,7 @@ impl Server {
.add_request_handler(user_handler(rejoin_remote_projects))
.add_request_handler(user_handler(create_remote_project))
.add_request_handler(user_handler(create_dev_server))
.add_request_handler(user_handler(delete_dev_server))
.add_request_handler(dev_server_handler(share_remote_project))
.add_request_handler(dev_server_handler(shutdown_dev_server))
.add_request_handler(dev_server_handler(reconnect_dev_server))
@ -1044,12 +1052,14 @@ impl Server {
.await?;
}
let (contacts, channels_for_user, channel_invites) = future::try_join3(
self.app_state.db.get_contacts(user.id),
self.app_state.db.get_channels_for_user(user.id),
self.app_state.db.get_channel_invites_for_user(user.id),
)
.await?;
let (contacts, channels_for_user, channel_invites, remote_projects) =
future::try_join4(
self.app_state.db.get_contacts(user.id),
self.app_state.db.get_channels_for_user(user.id),
self.app_state.db.get_channel_invites_for_user(user.id),
self.app_state.db.remote_projects_update(user.id),
)
.await?;
{
let mut pool = self.connection_pool.lock();
@ -1067,9 +1077,10 @@ impl Server {
)?;
self.peer.send(
connection_id,
build_channels_update(channels_for_user, channel_invites, &pool),
build_channels_update(channels_for_user, channel_invites),
)?;
}
send_remote_projects_update(user.id, remote_projects, session).await;
if let Some(incoming_call) =
self.app_state.db.incoming_call_for_user(user.id).await?
@ -1087,9 +1098,6 @@ impl Server {
};
pool.add_dev_server(connection_id, dev_server.id, zed_version);
}
update_dev_server_status(dev_server, proto::DevServerStatus::Online, &session)
.await;
// todo!() allow only one connection.
let projects = self
.app_state
@ -1098,6 +1106,13 @@ impl Server {
.await?;
self.peer
.send(connection_id, proto::DevServerInstructions { projects })?;
let status = self
.app_state
.db
.remote_projects_update(dev_server.user_id)
.await?;
send_remote_projects_update(dev_server.user_id, status, &session).await;
}
}
@ -1401,10 +1416,8 @@ async fn connection_lost(
update_user_contacts(session.user_id(), &session).await?;
},
Principal::DevServer(dev_server) => {
lost_dev_server_connection(&session).await?;
update_dev_server_status(&dev_server, proto::DevServerStatus::Offline, &session)
.await;
Principal::DevServer(_) => {
lost_dev_server_connection(&session.for_dev_server().unwrap()).await?;
},
}
},
@ -1941,6 +1954,9 @@ async fn share_project(
RoomId::from_proto(request.room_id),
session.connection_id,
&request.worktrees,
request
.remote_project_id
.map(|id| RemoteProjectId::from_proto(id)),
)
.await?;
response.send(proto::ShareProjectResponse {
@ -1954,14 +1970,25 @@ async fn share_project(
/// Unshare a project from the room.
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(message.project_id);
unshare_project_internal(project_id, &session).await
unshare_project_internal(
project_id,
session.connection_id,
session.user_id(),
&session,
)
.await
}
async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> Result<()> {
async fn unshare_project_internal(
project_id: ProjectId,
connection_id: ConnectionId,
user_id: Option<UserId>,
session: &Session,
) -> Result<()> {
let (room, guest_connection_ids) = &*session
.db()
.await
.unshare_project(project_id, session.connection_id)
.unshare_project(project_id, connection_id, user_id)
.await?;
let message = proto::UnshareProject {
@ -1969,7 +1996,7 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
};
broadcast(
Some(session.connection_id),
Some(connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
@ -1980,13 +2007,13 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
Ok(())
}
/// Share a project into the room.
/// DevServer makes a project available online
async fn share_remote_project(
request: proto::ShareRemoteProject,
response: Response<proto::ShareRemoteProject>,
session: DevServerSession,
) -> Result<()> {
let remote_project = session
let (remote_project, user_id, status) = session
.db()
.await
.share_remote_project(
@ -2000,22 +2027,7 @@ async fn share_remote_project(
return Err(anyhow!("failed to share remote project"))?;
};
for (connection_id, _) in session
.connection_pool()
.await
.channel_connection_ids(ChannelId::from_proto(remote_project.channel_id))
{
session
.peer
.send(
connection_id,
proto::UpdateChannels {
remote_projects: vec![remote_project.clone()],
..Default::default()
},
)
.trace_err();
}
send_remote_projects_update(user_id, status, &session).await;
response.send(proto::ShareProjectResponse { project_id })?;
@ -2081,19 +2093,21 @@ fn join_project_internal(
})
.collect::<Vec<_>>();
let add_project_collaborator = proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
}),
};
for collaborator in &collaborators {
session
.peer
.send(
collaborator.peer_id.unwrap().into(),
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
}),
},
add_project_collaborator.clone(),
)
.trace_err();
}
@ -2105,7 +2119,10 @@ fn join_project_internal(
replica_id: replica_id.0 as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
role: project.role.into(), // todo
role: project.role.into(),
remote_project_id: project
.remote_project_id
.map(|remote_project_id| remote_project_id.0 as u64),
})?;
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
@ -2188,8 +2205,6 @@ async fn leave_project(request: proto::LeaveProject, session: UserSession) -> Re
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
tracing::info!(
%project_id,
host_user_id = ?project.host_user_id,
host_connection_id = ?project.host_connection_id,
"leave project"
);
@ -2224,13 +2239,33 @@ async fn create_remote_project(
response: Response<proto::CreateRemoteProject>,
session: UserSession,
) -> Result<()> {
let (channel, remote_project) = session
let dev_server_id = DevServerId(request.dev_server_id as i32);
let dev_server_connection_id = session
.connection_pool()
.await
.dev_server_connection_id(dev_server_id);
let Some(dev_server_connection_id) = dev_server_connection_id else {
Err(ErrorCode::DevServerOffline
.message("Cannot create a remote project when the dev server is offline".to_string())
.anyhow())?
};
let path = request.path.clone();
//Check that the path exists on the dev server
session
.peer
.forward_request(
session.connection_id,
dev_server_connection_id,
proto::ValidateRemoteProjectRequest { path: path.clone() },
)
.await?;
let (remote_project, update) = session
.db()
.await
.create_remote_project(
ChannelId(request.channel_id as i32),
DevServerId(request.dev_server_id as i32),
&request.name,
&request.path,
session.user_id(),
)
@ -2242,25 +2277,12 @@ async fn create_remote_project(
.get_remote_projects_for_dev_server(remote_project.dev_server_id)
.await?;
let update = proto::UpdateChannels {
remote_projects: vec![remote_project.to_proto(None)],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) {
if role.can_see_all_descendants() {
session.peer.send(connection_id, update.clone())?;
}
}
session.peer.send(
dev_server_connection_id,
proto::DevServerInstructions { projects },
)?;
let dev_server_id = remote_project.dev_server_id;
let dev_server_connection_id = connection_pool.dev_server_connection_id(dev_server_id);
if let Some(dev_server_connection_id) = dev_server_connection_id {
session.peer.send(
dev_server_connection_id,
proto::DevServerInstructions { projects },
)?;
}
send_remote_projects_update(session.user_id(), update, &session).await;
response.send(proto::CreateRemoteProjectResponse {
remote_project: Some(remote_project.to_proto(None)),
@ -2276,37 +2298,56 @@ async fn create_dev_server(
let access_token = auth::random_token();
let hashed_access_token = auth::hash_access_token(&access_token);
let (channel, dev_server) = session
let (dev_server, status) = session
.db()
.await
.create_dev_server(
ChannelId(request.channel_id as i32),
&request.name,
&hashed_access_token,
session.user_id(),
)
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
.await?;
let update = proto::UpdateChannels {
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) {
if role.can_see_channel(channel.visibility) {
session.peer.send(connection_id, update.clone())?;
}
}
send_remote_projects_update(session.user_id(), status, &session).await;
response.send(proto::CreateDevServerResponse {
dev_server_id: dev_server.id.0 as u64,
channel_id: request.channel_id,
access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token),
name: request.name.clone(),
})?;
Ok(())
}
async fn delete_dev_server(
request: proto::DeleteDevServer,
response: Response<proto::DeleteDevServer>,
session: UserSession,
) -> Result<()> {
let dev_server_id = DevServerId(request.dev_server_id as i32);
let dev_server = session.db().await.get_dev_server(dev_server_id).await?;
if dev_server.user_id != session.user_id() {
return Err(anyhow!(ErrorCode::Forbidden))?;
}
let connection_id = session
.connection_pool()
.await
.dev_server_connection_id(dev_server_id);
if let Some(connection_id) = connection_id {
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
}
let status = session
.db()
.await
.delete_dev_server(dev_server_id, session.user_id())
.await?;
send_remote_projects_update(session.user_id(), status, &session).await;
response.send(proto::Ack {})?;
Ok(())
}
async fn rejoin_remote_projects(
request: proto::RejoinRemoteProjects,
response: Response<proto::RejoinRemoteProjects>,
@ -2403,8 +2444,15 @@ async fn shutdown_dev_server(
session: DevServerSession,
) -> Result<()> {
response.send(proto::Ack {})?;
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await
}
async fn shutdown_dev_server_internal(
dev_server_id: DevServerId,
connection_id: ConnectionId,
session: &Session,
) -> Result<()> {
let (remote_projects, dev_server) = {
let dev_server_id = session.dev_server_id();
let db = session.db().await;
let remote_projects = db.get_remote_projects_for_dev_server(dev_server_id).await?;
let dev_server = db.get_dev_server(dev_server_id).await?;
@ -2412,22 +2460,26 @@ async fn shutdown_dev_server(
};
for project_id in remote_projects.iter().filter_map(|p| p.project_id) {
unshare_project_internal(ProjectId::from_proto(project_id), &session.0).await?;
unshare_project_internal(
ProjectId::from_proto(project_id),
connection_id,
None,
session,
)
.await?;
}
let update = proto::UpdateChannels {
remote_projects,
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
..Default::default()
};
for (connection_id, _) in session
session
.connection_pool()
.await
.channel_connection_ids(dev_server.channel_id)
{
session.peer.send(connection_id, update.clone()).trace_err();
}
.set_dev_server_offline(dev_server_id);
let status = session
.db()
.await
.remote_projects_update(dev_server.user_id)
.await?;
send_remote_projects_update(dev_server.user_id, status, &session).await;
Ok(())
}
@ -4626,7 +4678,7 @@ fn notify_membership_updated(
..Default::default()
};
let mut update = build_channels_update(result.new_channels, vec![], connection_pool);
let mut update = build_channels_update(result.new_channels, vec![]);
update.delete_channels = result
.removed_channels
.into_iter()
@ -4659,7 +4711,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
fn build_channels_update(
channels: ChannelsForUser,
channel_invites: Vec<db::Channel>,
pool: &ConnectionPool,
) -> proto::UpdateChannels {
let mut update = proto::UpdateChannels::default();
@ -4684,13 +4735,6 @@ fn build_channels_update(
}
update.hosted_projects = channels.hosted_projects;
update.dev_servers = channels
.dev_servers
.into_iter()
.map(|dev_server| dev_server.to_proto(pool.dev_server_status(dev_server.id)))
.collect();
update.remote_projects = channels.remote_projects;
update
}
@ -4777,24 +4821,19 @@ fn channel_updated(
);
}
async fn update_dev_server_status(
dev_server: &dev_server::Model,
status: proto::DevServerStatus,
async fn send_remote_projects_update(
user_id: UserId,
mut status: proto::RemoteProjectsUpdate,
session: &Session,
) {
let pool = session.connection_pool().await;
let connections = pool.channel_connection_ids(dev_server.channel_id);
for (connection_id, _) in connections {
session
.peer
.send(
connection_id,
proto::UpdateChannels {
dev_servers: vec![dev_server.to_proto(status)],
..Default::default()
},
)
.trace_err();
for dev_server in &mut status.dev_servers {
dev_server.status =
pool.dev_server_status(DevServerId(dev_server.dev_server_id as i32)) as i32;
}
let connections = pool.user_connection_ids(user_id);
for connection_id in connections {
session.peer.send(connection_id, status.clone()).trace_err();
}
}
@ -4833,7 +4872,7 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()>
Ok(())
}
async fn lost_dev_server_connection(session: &Session) -> Result<()> {
async fn lost_dev_server_connection(session: &DevServerSession) -> Result<()> {
log::info!("lost dev server connection, unsharing projects");
let project_ids = session
.db()
@ -4843,9 +4882,14 @@ async fn lost_dev_server_connection(session: &Session) -> Result<()> {
for project_id in project_ids {
// not unshare re-checks the connection ids match, so we get away with no transaction
unshare_project_internal(project_id, &session).await?;
unshare_project_internal(project_id, session.connection_id, None, &session).await?;
}
let user_id = session.dev_server().user_id;
let update = session.db().await.remote_projects_update(user_id).await?;
send_remote_projects_update(user_id, update, session).await;
Ok(())
}
@ -4947,7 +4991,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
fn project_left(project: &db::LeftProject, session: &UserSession) {
for connection_id in &project.connection_ids {
if project.host_user_id == Some(session.user_id()) {
if project.should_unshare {
session
.peer
.send(

View file

@ -13,6 +13,7 @@ pub struct ConnectionPool {
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
channels: ChannelPool,
offline_dev_servers: HashSet<DevServerId>,
}
#[derive(Default, Serialize)]
@ -106,12 +107,17 @@ impl ConnectionPool {
}
PrincipalId::DevServerId(dev_server_id) => {
self.connected_dev_servers.remove(&dev_server_id);
self.offline_dev_servers.remove(&dev_server_id);
}
}
self.connections.remove(&connection_id).unwrap();
Ok(())
}
pub fn set_dev_server_offline(&mut self, dev_server_id: DevServerId) {
self.offline_dev_servers.insert(dev_server_id);
}
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
self.connections.values()
}
@ -137,7 +143,9 @@ impl ConnectionPool {
}
pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus {
if self.dev_server_connection_id(dev_server_id).is_some() {
if self.dev_server_connection_id(dev_server_id).is_some()
&& !self.offline_dev_servers.contains(&dev_server_id)
{
proto::DevServerStatus::Online
} else {
proto::DevServerStatus::Offline

View file

@ -1023,6 +1023,8 @@ async fn test_channel_link_notifications(
.await
.unwrap();
executor.run_until_parked();
// the new channel shows for b and c
assert_channels_list_shape(
client_a.channel_store(),

View file

@ -1,45 +1,40 @@
use std::path::Path;
use std::{path::Path, sync::Arc};
use call::ActiveCall;
use editor::Editor;
use fs::Fs;
use gpui::VisualTestContext;
use rpc::proto::DevServerStatus;
use gpui::{TestAppContext, VisualTestContext, WindowHandle};
use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt};
use serde_json::json;
use workspace::{AppState, Workspace};
use crate::tests::TestServer;
use crate::tests::{following_tests::join_channel, TestServer};
use super::TestClient;
#[gpui::test]
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client) = TestServer::start1(cx).await;
let channel_id = server
.make_channel("test", None, (&client, cx), &mut [])
.await;
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
let resp = client
.channel_store()
let resp = store
.update(cx, |store, cx| {
store.create_dev_server(channel_id, "server-1".to_string(), cx)
store.create_dev_server("server-1".to_string(), cx)
})
.await
.unwrap();
client.channel_store().update(cx, |store, _| {
assert_eq!(store.dev_servers_for_id(channel_id).len(), 1);
assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1");
assert_eq!(
store.dev_servers_for_id(channel_id)[0].status,
DevServerStatus::Offline
);
store.update(cx, |store, _| {
assert_eq!(store.dev_servers().len(), 1);
assert_eq!(store.dev_servers()[0].name, "server-1");
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Offline);
});
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
cx.executor().run_until_parked();
client.channel_store().update(cx, |store, _| {
assert_eq!(
store.dev_servers_for_id(channel_id)[0].status,
DevServerStatus::Online
);
store.update(cx, |store, _| {
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Online);
});
dev_server
@ -54,13 +49,10 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
)
.await;
client
.channel_store()
store
.update(cx, |store, cx| {
store.create_remote_project(
channel_id,
client::DevServerId(resp.dev_server_id),
"project-1".to_string(),
"/remote".to_string(),
cx,
)
@ -70,12 +62,11 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
cx.executor().run_until_parked();
let remote_workspace = client
.channel_store()
let remote_workspace = store
.update(cx, |store, cx| {
let projects = store.remote_projects_for_id(channel_id);
let projects = store.remote_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].name, "project-1");
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client.app_state.clone(),
@ -87,19 +78,19 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
cx.executor().run_until_parked();
let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
cx2.simulate_keystrokes("cmd-p 1 enter");
let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
cx.simulate_keystrokes("cmd-p 1 enter");
let editor = remote_workspace
.update(cx2, |ws, cx| {
.update(cx, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
})
.unwrap();
editor.update(cx2, |ed, cx| {
editor.update(cx, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
});
cx2.simulate_input("wow!");
cx2.simulate_keystrokes("cmd-s");
cx.simulate_input("wow!");
cx.simulate_keystrokes("cmd-s");
let content = dev_server
.fs()
@ -108,3 +99,263 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
.unwrap();
assert_eq!(content, "wow!remote\nremote\nremote\n");
}
#[gpui::test]
async fn test_dev_server_env_files(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.executor().run_until_parked();
let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
cx1.simulate_keystrokes("cmd-p . e enter");
let editor = remote_workspace
.update(cx1, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
})
.unwrap();
editor.update(cx1, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "SECRET");
});
cx1.update(|cx| {
workspace::join_channel(
channel_id,
client1.app_state.clone(),
Some(remote_workspace),
cx,
)
})
.await
.unwrap();
cx1.executor().run_until_parked();
remote_workspace
.update(cx1, |ws, cx| {
assert!(ws.project().read(cx).is_shared());
})
.unwrap();
join_channel(channel_id, &client2, cx2).await.unwrap();
cx2.executor().run_until_parked();
let (workspace2, cx2) = client2.active_workspace(cx2);
let editor = workspace2.update(cx2, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
});
// TODO: it'd be nice to hide .env files from other people
editor.update(cx2, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "SECRET");
});
}
async fn create_remote_project(
server: &TestServer,
client_app_state: Arc<AppState>,
cx: &mut TestAppContext,
cx_devserver: &mut TestAppContext,
) -> (TestClient, WindowHandle<Workspace>) {
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
let resp = store
.update(cx, |store, cx| {
store.create_dev_server("server-1".to_string(), cx)
})
.await
.unwrap();
let dev_server = server
.create_dev_server(resp.access_token, cx_devserver)
.await;
cx.executor().run_until_parked();
dev_server
.fs()
.insert_tree(
"/remote",
json!({
"1.txt": "remote\nremote\nremote",
".env": "SECRET",
}),
)
.await;
store
.update(cx, |store, cx| {
store.create_remote_project(
client::DevServerId(resp.dev_server_id),
"/remote".to_string(),
cx,
)
})
.await
.unwrap();
cx.executor().run_until_parked();
let workspace = store
.update(cx, |store, cx| {
let projects = store.remote_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
})
.await
.unwrap();
cx.executor().run_until_parked();
(dev_server, workspace)
}
#[gpui::test]
async fn test_dev_server_leave_room(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
let (_dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
channel_id,
client1.app_state.clone(),
Some(remote_workspace),
cx,
)
})
.await
.unwrap();
cx1.executor().run_until_parked();
remote_workspace
.update(cx1, |ws, cx| {
assert!(ws.project().read(cx).is_shared());
})
.unwrap();
join_channel(channel_id, &client2, cx2).await.unwrap();
cx2.executor().run_until_parked();
cx1.update(|cx| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(cx)))
.await
.unwrap();
cx1.executor().run_until_parked();
let (workspace, cx2) = client2.active_workspace(cx2);
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
}
#[gpui::test]
async fn test_dev_server_reconnect(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (mut server, client1) = TestServer::start1(cx1).await;
let channel_id = server
.make_channel("test", None, (&client1, cx1), &mut [])
.await;
let (_dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
cx1.update(|cx| {
workspace::join_channel(
channel_id,
client1.app_state.clone(),
Some(remote_workspace),
cx,
)
})
.await
.unwrap();
cx1.executor().run_until_parked();
remote_workspace
.update(cx1, |ws, cx| {
assert!(ws.project().read(cx).is_shared());
})
.unwrap();
drop(client1);
let client2 = server.create_client(cx2, "user_a").await;
let store = cx2.update(|cx| remote_projects::Store::global(cx).clone());
store
.update(cx2, |store, cx| {
let projects = store.remote_projects();
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client2.app_state.clone(),
cx,
)
})
.await
.unwrap();
}
#[gpui::test]
async fn test_create_remote_project_path_validation(
cx1: &mut gpui::TestAppContext,
cx2: &mut gpui::TestAppContext,
cx3: &mut gpui::TestAppContext,
) {
let (server, client1) = TestServer::start1(cx1).await;
let _channel_id = server
.make_channel("test", None, (&client1, cx1), &mut [])
.await;
// Creating a project with a path that does exist should not fail
let (_dev_server, _) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
cx1.executor().run_until_parked();
let store = cx1.update(|cx| remote_projects::Store::global(cx).clone());
let resp = store
.update(cx1, |store, cx| {
store.create_dev_server("server-2".to_string(), cx)
})
.await
.unwrap();
cx1.executor().run_until_parked();
let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
cx1.executor().run_until_parked();
// Creating a remote project with a path that does not exist should fail
let result = store
.update(cx1, |store, cx| {
store.create_remote_project(
client::DevServerId(resp.dev_server_id),
"/notfound".to_string(),
cx,
)
})
.await;
cx1.executor().run_until_parked();
let error = result.unwrap_err();
assert!(matches!(
error.error_code(),
ErrorCode::RemoteProjectPathDoesNotExist
));
}

View file

@ -3743,6 +3743,10 @@ async fn test_leaving_project(
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 2);
});
// Drop client B's connection and ensure client A and client C observe client B leaving.
client_b.disconnect(&cx_b.to_async());
executor.advance_clock(RECONNECT_TIMEOUT);

View file

@ -284,6 +284,7 @@ impl TestServer {
collab_ui::init(&app_state, cx);
file_finder::init(cx);
menu::init();
remote_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
});