Detailed changes
@@ -484,14 +484,20 @@ async fn test_offline_projects(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
let user_a = UserId::from_proto(client_a.user_id().unwrap());
server
- .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+ .make_contacts(vec![
+ (&client_a, cx_a),
+ (&client_b, cx_b),
+ (&client_c, cx_c),
+ ])
.await;
// Set up observers of the project and user stores. Any time either of
@@ -585,7 +591,8 @@ async fn test_offline_projects(
.await
.unwrap();
- // When a project is offline, no information about it is sent to the server.
+ // When a project is offline, we still create it on the server but is invisible
+ // to other users.
deterministic.run_until_parked();
assert!(server
.store
@@ -593,7 +600,10 @@ async fn test_offline_projects(
.await
.project_metadata_for_user(user_a)
.is_empty());
- assert!(project.read_with(cx_a, |project, _| project.remote_id().is_none()));
+ project.read_with(cx_a, |project, _| {
+ assert!(project.remote_id().is_some());
+ assert!(!project.is_online());
+ });
assert!(client_b
.user_store
.read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
@@ -667,7 +677,7 @@ async fn test_offline_projects(
// Build another project using a directory which was previously part of
// an online project. Restore the project's state from the host's database.
- let project2 = cx_a.update(|cx| {
+ let project2_a = cx_a.update(|cx| {
Project::local(
false,
client_a.client.clone(),
@@ -678,21 +688,21 @@ async fn test_offline_projects(
cx,
)
});
- project2
+ project2_a
.update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/code/crate3", true, cx)
})
.await
.unwrap();
- project2
+ project2_a
.update(cx_a, |project, cx| project.restore_state(cx))
.await
.unwrap();
// This project is now online, because its directory was previously online.
- project2.read_with(cx_a, |project, _| assert!(project.is_online()));
+ project2_a.read_with(cx_a, |project, _| assert!(project.is_online()));
deterministic.run_until_parked();
- let project2_id = project2.read_with(cx_a, |p, _| p.remote_id()).unwrap();
+ let project2_id = project2_a.read_with(cx_a, |p, _| p.remote_id()).unwrap();
client_b.user_store.read_with(cx_b, |store, _| {
assert_eq!(
store.contacts()[0].projects,
@@ -715,6 +725,41 @@ async fn test_offline_projects(
);
});
+ let project2_b = client_b.build_remote_project(&project2_a, cx_a, cx_b).await;
+ let project2_c = cx_c.foreground().spawn(Project::remote(
+ project2_id,
+ client_c.client.clone(),
+ client_c.user_store.clone(),
+ client_c.project_store.clone(),
+ client_c.language_registry.clone(),
+ FakeFs::new(cx_c.background()),
+ cx_c.to_async(),
+ ));
+ deterministic.run_until_parked();
+
+ // Taking a project offline unshares the project, rejects any pending join request and
+ // disconnects existing guests.
+ project2_a.update(cx_a, |project, cx| project.set_online(false, cx));
+ deterministic.run_until_parked();
+ project2_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
+ project2_b.read_with(cx_b, |project, _| assert!(project.is_read_only()));
+ project2_c.await.unwrap_err();
+
+ client_b.user_store.read_with(cx_b, |store, _| {
+ assert_eq!(
+ store.contacts()[0].projects,
+ &[ProjectMetadata {
+ id: project_id,
+ visible_worktree_root_names: vec![
+ "crate1".into(),
+ "crate2".into(),
+ "crate3".into()
+ ],
+ guests: Default::default(),
+ },]
+ );
+ });
+
cx_a.update(|cx| {
drop(subscriptions);
drop(view);
@@ -605,9 +605,11 @@ impl Server {
.await
.user_id_for_connection(request.sender_id)?;
let project_id = self.app_state.db.register_project(user_id).await?;
- self.store_mut()
- .await
- .register_project(request.sender_id, project_id)?;
+ self.store_mut().await.register_project(
+ request.sender_id,
+ project_id,
+ request.payload.online,
+ )?;
response.send(proto::RegisterProjectResponse {
project_id: project_id.to_proto(),
@@ -925,12 +927,53 @@ impl Server {
let guest_connection_ids = state
.read_project(project_id, request.sender_id)?
.guest_connection_ids();
- state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
- broadcast(request.sender_id, guest_connection_ids, |connection_id| {
- self.peer
- .forward_send(request.sender_id, connection_id, request.payload.clone())
- });
+ let unshared_project = state.update_project(
+ project_id,
+ &request.payload.worktrees,
+ request.payload.online,
+ request.sender_id,
+ )?;
+
+ if let Some(unshared_project) = unshared_project {
+ broadcast(
+ request.sender_id,
+ unshared_project.guests.keys().copied(),
+ |conn_id| {
+ self.peer.send(
+ conn_id,
+ proto::UnregisterProject {
+ project_id: project_id.to_proto(),
+ },
+ )
+ },
+ );
+ for (_, receipts) in unshared_project.pending_join_requests {
+ for receipt in receipts {
+ self.peer.respond(
+ receipt,
+ proto::JoinProjectResponse {
+ variant: Some(proto::join_project_response::Variant::Decline(
+ proto::join_project_response::Decline {
+ reason:
+ proto::join_project_response::decline::Reason::Closed
+ as i32,
+ },
+ )),
+ },
+ )?;
+ }
+ }
+ } else {
+ broadcast(request.sender_id, guest_connection_ids, |connection_id| {
+ self.peer.forward_send(
+ request.sender_id,
+ connection_id,
+ request.payload.clone(),
+ )
+ });
+ }
};
+
self.update_user_contacts(user_id).await?;
Ok(())
}
@@ -32,6 +32,7 @@ struct ConnectionState {
#[derive(Serialize)]
pub struct Project {
+ pub online: bool,
pub host_connection_id: ConnectionId,
pub host: Collaborator,
pub guests: HashMap<ConnectionId, Collaborator>,
@@ -88,6 +89,11 @@ pub struct LeftProject {
pub unshare: bool,
}
+pub struct UnsharedProject {
+ pub guests: HashMap<ConnectionId, Collaborator>,
+ pub pending_join_requests: HashMap<UserId, Vec<Receipt<proto::JoinProject>>>,
+}
+
#[derive(Copy, Clone)]
pub struct Metrics {
pub connections: usize,
@@ -297,7 +303,7 @@ impl Store {
let mut metadata = Vec::new();
for project_id in project_ids {
if let Some(project) = self.projects.get(&project_id) {
- if project.host.user_id == user_id {
+ if project.host.user_id == user_id && project.online {
metadata.push(proto::ProjectMetadata {
id: project_id.to_proto(),
visible_worktree_root_names: project
@@ -323,6 +329,7 @@ impl Store {
&mut self,
host_connection_id: ConnectionId,
project_id: ProjectId,
+ online: bool,
) -> Result<()> {
let connection = self
.connections
@@ -332,6 +339,7 @@ impl Store {
self.projects.insert(
project_id,
Project {
+ online,
host_connection_id,
host: Collaborator {
user_id: connection.user_id,
@@ -353,8 +361,9 @@ impl Store {
&mut self,
project_id: ProjectId,
worktrees: &[proto::WorktreeMetadata],
+ online: bool,
connection_id: ConnectionId,
- ) -> Result<()> {
+ ) -> Result<Option<UnsharedProject>> {
let project = self
.projects
.get_mut(&project_id)
@@ -375,7 +384,34 @@ impl Store {
);
}
}
- Ok(())
+
+ if online != project.online {
+ project.online = online;
+ if project.online {
+ Ok(None)
+ } else {
+ for connection_id in project.guest_connection_ids() {
+ if let Some(connection) = self.connections.get_mut(&connection_id) {
+ connection.projects.remove(&project_id);
+ }
+ }
+
+ project.active_replica_ids.clear();
+ project.language_servers.clear();
+ for worktree in project.worktrees.values_mut() {
+ worktree.diagnostic_summaries.clear();
+ worktree.entries.clear();
+ worktree.extension_counts.clear();
+ }
+
+ Ok(Some(UnsharedProject {
+ guests: mem::take(&mut project.guests),
+ pending_join_requests: mem::take(&mut project.join_requests),
+ }))
+ }
+ } else {
+ Ok(None)
+ }
} else {
Err(anyhow!("no such project"))?
}
@@ -481,13 +517,17 @@ impl Store {
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
- connection.requested_projects.insert(project_id);
- project
- .join_requests
- .entry(requester_id)
- .or_default()
- .push(receipt);
- Ok(())
+ if project.online {
+ connection.requested_projects.insert(project_id);
+ project
+ .join_requests
+ .entry(requester_id)
+ .or_default()
+ .push(receipt);
+ Ok(())
+ } else {
+ Err(anyhow!("no such project"))
+ }
}
pub fn deny_join_project_request(
@@ -136,13 +136,14 @@ enum ProjectClientState {
remote_id_rx: watch::Receiver<Option<u64>>,
online_tx: watch::Sender<bool>,
online_rx: watch::Receiver<bool>,
- _maintain_remote_id_task: Task<Option<()>>,
+ _maintain_remote_id: Task<Option<()>>,
+ _maintain_online_status: Task<Option<()>>,
},
Remote {
sharing_has_stopped: bool,
remote_id: u64,
replica_id: ReplicaId,
- _detect_unshare_task: Task<Option<()>>,
+ _detect_unshare: Task<Option<()>>,
},
}
@@ -381,17 +382,13 @@ impl Project {
cx: &mut MutableAppContext,
) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| {
- let (online_tx, online_rx) = watch::channel_with(online);
let (remote_id_tx, remote_id_rx) = watch::channel();
- let _maintain_remote_id_task = cx.spawn_weak({
- let status_rx = client.clone().status();
- let online_rx = online_rx.clone();
+ let _maintain_remote_id = cx.spawn_weak({
+ let mut status_rx = client.clone().status();
move |this, mut cx| async move {
- let mut stream = Stream::map(status_rx.clone(), drop)
- .merge(Stream::map(online_rx.clone(), drop));
- while stream.recv().await.is_some() {
+ while let Some(status) = status_rx.recv().await {
let this = this.upgrade(&cx)?;
- if status_rx.borrow().is_connected() && *online_rx.borrow() {
+ if status.is_connected() {
this.update(&mut cx, |this, cx| this.register(cx))
.await
.log_err()?;
@@ -405,6 +402,23 @@ impl Project {
}
});
+ let (online_tx, online_rx) = watch::channel_with(online);
+ let _maintain_online_status = cx.spawn_weak({
+ let mut online_rx = online_rx.clone();
+ move |this, mut cx| async move {
+ while online_rx.recv().await.is_some() {
+ let this = this.upgrade(&cx)?;
+ this.update(&mut cx, |this, cx| {
+ if !this.is_online() {
+ this.unshared(cx);
+ }
+ this.metadata_changed(false, cx)
+ });
+ }
+ None
+ }
+ });
+
let handle = cx.weak_handle();
project_store.update(cx, |store, cx| store.add_project(handle, cx));
@@ -423,7 +437,8 @@ impl Project {
remote_id_rx,
online_tx,
online_rx,
- _maintain_remote_id_task,
+ _maintain_remote_id,
+ _maintain_online_status,
},
opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx),
client_subscriptions: Vec::new(),
@@ -519,7 +534,7 @@ impl Project {
sharing_has_stopped: false,
remote_id,
replica_id,
- _detect_unshare_task: cx.spawn_weak(move |this, mut cx| {
+ _detect_unshare: cx.spawn_weak(move |this, mut cx| {
async move {
let mut status = client.status();
let is_connected =
@@ -850,27 +865,36 @@ impl Project {
}
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
- if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state {
+ if let ProjectClientState::Local {
+ remote_id_rx,
+ online_rx,
+ ..
+ } = &self.client_state
+ {
if remote_id_rx.borrow().is_some() {
return Task::ready(Ok(()));
}
- }
- let response = self.client.request(proto::RegisterProject {});
- cx.spawn(|this, mut cx| async move {
- let remote_id = response.await?.project_id;
- this.update(&mut cx, |this, cx| {
- if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
- *remote_id_tx.borrow_mut() = Some(remote_id);
- }
+ let response = self.client.request(proto::RegisterProject {
+ online: *online_rx.borrow(),
+ });
+ cx.spawn(|this, mut cx| async move {
+ let remote_id = response.await?.project_id;
+ this.update(&mut cx, |this, cx| {
+ if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
+ *remote_id_tx.borrow_mut() = Some(remote_id);
+ }
- this.metadata_changed(false, cx);
- cx.emit(Event::RemoteIdChanged(Some(remote_id)));
- this.client_subscriptions
- .push(this.client.add_model_for_remote_entity(remote_id, cx));
- Ok(())
+ this.metadata_changed(false, cx);
+ cx.emit(Event::RemoteIdChanged(Some(remote_id)));
+ this.client_subscriptions
+ .push(this.client.add_model_for_remote_entity(remote_id, cx));
+ Ok(())
+ })
})
- })
+ } else {
+ Task::ready(Err(anyhow!("can't register a remote project")))
+ }
}
pub fn remote_id(&self) -> Option<u64> {
@@ -934,19 +958,25 @@ impl Project {
..
} = &self.client_state
{
- if let (Some(project_id), true) = (*remote_id_rx.borrow(), *online_rx.borrow()) {
+ // Broadcast worktrees only if the project is public.
+ let worktrees = if *online_rx.borrow() {
+ self.worktrees
+ .iter()
+ .filter_map(|worktree| {
+ worktree
+ .upgrade(&cx)
+ .map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
+ })
+ .collect()
+ } else {
+ Default::default()
+ };
+ if let Some(project_id) = *remote_id_rx.borrow() {
self.client
.send(proto::UpdateProject {
project_id,
- worktrees: self
- .worktrees
- .iter()
- .filter_map(|worktree| {
- worktree.upgrade(&cx).map(|worktree| {
- worktree.read(cx).as_local().unwrap().metadata_proto()
- })
- })
- .collect(),
+ worktrees,
+ online: *online_rx.borrow(),
})
.log_err();
}
@@ -121,7 +121,9 @@ message Test {
uint64 id = 1;
}
-message RegisterProject {}
+message RegisterProject {
+ bool online = 1;
+}
message RegisterProjectResponse {
uint64 project_id = 1;
@@ -134,6 +136,7 @@ message UnregisterProject {
message UpdateProject {
uint64 project_id = 1;
repeated WorktreeMetadata worktrees = 2;
+ bool online = 3;
}
message RegisterProjectActivity {
@@ -130,9 +130,9 @@ messages!(
(PrepareRename, Background),
(PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground),
+ (ProjectUnshared, Foreground),
(RegisterProjectResponse, Foreground),
(Ping, Foreground),
- (ProjectUnshared, Foreground),
(RegisterProject, Foreground),
(RegisterProjectActivity, Foreground),
(ReloadBuffers, Foreground),
@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 26;
+pub const PROTOCOL_VERSION: u32 = 27;