diff --git a/daemon/src/client.rs b/daemon/src/client.rs index fa96d74..63044a2 100644 --- a/daemon/src/client.rs +++ b/daemon/src/client.rs @@ -35,8 +35,8 @@ pub fn is_active( client_entity: entity::Entity, ) -> bool { world - .get::(seat_entity) - .is_some_and(|active_client| active_client.0 == client_entity) + .get::(seat_entity) + .is_some_and(|active_client| active_client.0.contains(&client_entity)) } pub fn session_id( @@ -72,8 +72,8 @@ pub fn add_object( object.send_active(u32::from(is_active(world, seat_entity, client_entity))); object.send_next(u32::from( world - .get::(seat_entity) - .is_some_and(|next| next.0 == client_entity), + .get::(seat_entity) + .is_some_and(|next| next.0.contains(&client_entity)), )); if let Some(pid) = world.get::(client_entity) { object.send_pid(pid.0); @@ -232,41 +232,27 @@ pub fn add( return Err(anyhow::anyhow!("client cannot be reused")); } + if let Some(pid) = world.get::(client_entity).map(|pid| pid.0) + && seat::clients(world, seat_entity).into_iter().any(|client| { + world + .get::(client) + .is_some_and(|client_pid| client_pid.0 == pid) + }) + { + return Err(anyhow::anyhow!("PID is already attached to the seat")); + } + let name = seat::name(world, seat_entity)?; seat::refresh_current_vt(world, seat_entity); let mode = seat::mode(world, seat_entity)?; let session_id = match mode { ecs::SeatKind::VtBound => { - if let Some(active_client) = world.get::(seat_entity) { - let active_state = world - .get::(active_client.0) - .map(|state| state.0); - if active_state != Some(seat::SeatState::PendingDisabled) { - log::error!("Could not add client: seat is VT-bound and has an active client"); - return Err(anyhow::anyhow!("seat is VT-bound and has an active client")); - } - } - let vt = world .get::(seat_entity) .map(|vt| vt.0) .ok_or_else(|| anyhow::anyhow!("could not determine VT for client"))?; - let already_pending = seat::clients(world, seat_entity).into_iter().any(|client| { - world - .get::(client) - .is_some_and(|session| session.0.as_raw() == vt.get()) - }); - if world.get::(seat_entity).is_some() && already_pending { - log::error!( - "Could not add client: seat is VT-bound and already has pending client" - ); - return Err(anyhow::anyhow!( - "seat is VT-bound and already has pending client" - )); - } - session::SessionId::from_raw(vt.get()) } ecs::SeatKind::NonVtBound => { @@ -283,7 +269,13 @@ pub fn add( } }; - if mode == ecs::SeatKind::VtBound { + if mode == ecs::SeatKind::VtBound + && !seat::clients(world, seat_entity).into_iter().any(|client| { + world + .get::(client) + .is_some_and(|session| session.0 == session_id) + }) + { seat::vt_open(session_id.as_raw())?; } @@ -325,18 +317,6 @@ pub fn open( return Err(anyhow::anyhow!("client is not new or disabled")); } - if let Some(active_client) = world.get::(seat_entity) { - let active_state = world - .get::(active_client.0) - .map(|state| state.0); - if !seat::is_vt_bound(world, seat_entity) - || active_state != Some(seat::SeatState::PendingDisabled) - { - log::error!("Could not enable client: seat already has an active client"); - return Err(anyhow::anyhow!("seat already has an active client")); - } - } - if seat::is_vt_bound(world, seat_entity) { seat::vt_open(session_id.as_raw())?; } @@ -407,8 +387,14 @@ pub fn ack_disable( return Ok(()); } - seat::set_active_client(world, seat_entity, None); - let _ = seat::activate(world, seat_entity); + if let Some(mut active_clients) = world.get_mut::(seat_entity) { + active_clients.0.retain(|client| *client != client_entity); + let no_active_clients = active_clients.0.is_empty(); + if no_active_clients { + seat::set_active_client(world, seat_entity, None); + let _ = seat::activate(world, seat_entity); + } + } Ok(()) } diff --git a/daemon/src/ecs.rs b/daemon/src/ecs.rs index a0b5390..ac1f30e 100644 --- a/daemon/src/ecs.rs +++ b/daemon/src/ecs.rs @@ -66,8 +66,8 @@ impl NotifyBatch { #[derive(Resource, Default)] pub struct SeatNotifyState { - pub active_clients: HashMap, - pub next_clients: HashMap, + pub active_clients: HashMap>, + pub next_clients: HashMap>, } #[derive(Resource, Default)] @@ -337,6 +337,7 @@ pub struct SeatBundle { pub marker: Seat, pub name: SeatName, pub mode: SeatMode, + pub active_clients: ActiveClients, } #[derive(Bundle)] @@ -488,11 +489,11 @@ pub struct SeatMode(pub SeatKind); #[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] pub struct CurrentVt(pub num::NonZeroU32); -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] -pub struct ActiveClient(pub Entity); +#[derive(Component, Debug, Clone, PartialEq, Eq)] +pub struct ActiveClients(pub Vec); -#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] -pub struct NextClient(pub Entity); +#[derive(Component, Debug, Clone, PartialEq, Eq)] +pub struct NextClients(pub Vec); // Client components diff --git a/daemon/src/reconcile.rs b/daemon/src/reconcile.rs index 6772df5..b9904e0 100644 --- a/daemon/src/reconcile.rs +++ b/daemon/src/reconcile.rs @@ -74,37 +74,56 @@ fn notify_seat_clients( objects: Res, mut batch: ResMut, mut state: ResMut, - seats: Query<(Entity, Option<&ecs::ActiveClient>, Option<&ecs::NextClient>), With>, + seats: Query< + ( + Entity, + Option<&ecs::ActiveClients>, + Option<&ecs::NextClients>, + ), + With, + >, ) { - for (seat_entity, active_client, next_client) in &seats { - let active_client = active_client.map(|client| client.0); - let previous_active = match active_client { - Some(active_client) => state.active_clients.insert(seat_entity, active_client), - None => state.active_clients.remove(&seat_entity), - }; - - if previous_active != active_client { - if let Some(previous_active) = previous_active { - send_client_active(&objects, &mut batch, previous_active, false); - } - if let Some(active_client) = active_client { - send_client_active(&objects, &mut batch, active_client, true); - } + for (seat_entity, active_clients, next_clients) in &seats { + let active_clients = active_clients + .map(|clients| clients.0.clone()) + .filter(|clients| !clients.is_empty()) + .unwrap_or_default(); + let previous_active = state + .active_clients + .insert(seat_entity, active_clients.clone()) + .unwrap_or_default(); + for client in previous_active + .iter() + .filter(|client| !active_clients.contains(client)) + { + send_client_active(&objects, &mut batch, *client, false); + } + for client in active_clients + .iter() + .filter(|client| !previous_active.contains(client)) + { + send_client_active(&objects, &mut batch, *client, true); } - let next_client = next_client.map(|client| client.0); - let previous_next = match next_client { - Some(next_client) => state.next_clients.insert(seat_entity, next_client), - None => state.next_clients.remove(&seat_entity), - }; - - if previous_next != next_client { - if let Some(previous_next) = previous_next { - send_client_next(&objects, &mut batch, previous_next, false); - } - if let Some(next_client) = next_client { - send_client_next(&objects, &mut batch, next_client, true); - } + let next_clients = next_clients + .map(|clients| clients.0.clone()) + .filter(|clients| !clients.is_empty()) + .unwrap_or_default(); + let previous_next = state + .next_clients + .insert(seat_entity, next_clients.clone()) + .unwrap_or_default(); + for client in previous_next + .iter() + .filter(|client| !next_clients.contains(client)) + { + send_client_next(&objects, &mut batch, *client, false); + } + for client in next_clients + .iter() + .filter(|client| !previous_next.contains(client)) + { + send_client_next(&objects, &mut batch, *client, true); } } } diff --git a/daemon/src/seat.rs b/daemon/src/seat.rs index 33dfa42..863db4e 100644 --- a/daemon/src/seat.rs +++ b/daemon/src/seat.rs @@ -25,6 +25,7 @@ pub fn spawn( marker: ecs::Seat, name: ecs::SeatName(name), mode: ecs::SeatMode(mode), + active_clients: ecs::ActiveClients(Vec::new()), }); if let Some(current_vt) = num::NonZeroU32::new(current_vt) { @@ -197,7 +198,7 @@ pub fn set_next_session( return Ok(()); } - if world.get::(seat_entity).is_some() { + if world.get::(seat_entity).is_some() { log::info!("Could not set next session: switch is already queued"); return Ok(()); } @@ -212,18 +213,22 @@ pub fn set_next_session( return Ok(()); } - let Some(target) = clients(world, seat_entity).into_iter().find(|candidate| { - world - .get::(*candidate) - .is_some_and(|candidate_session| candidate_session.0 == session) - }) else { + let targets = clients(world, seat_entity) + .into_iter() + .filter(|candidate| { + world + .get::(*candidate) + .is_some_and(|candidate_session| candidate_session.0 == session) + }) + .collect::>(); + if targets.is_empty() { log::error!("Could not set next session: no such client"); return Err(anyhow::anyhow!("no such client")); - }; + } let name = name(world, seat_entity)?; log::info!("Queuing switch to client {} on {}", session.as_raw(), name); - set_next_client(world, seat_entity, Some(target)); + set_next_clients(world, seat_entity, Some(targets)); client::disable(world, seat_entity, client_entity)?; Ok(()) @@ -236,29 +241,35 @@ pub fn set_active_client( ) { match client_entity { Some(client_entity) => { - world - .entity_mut(seat_entity) - .insert(ecs::ActiveClient(client_entity)); + if let Some(mut active_clients) = world.get_mut::(seat_entity) { + if !active_clients.0.contains(&client_entity) { + active_clients.0.push(client_entity); + } + } else { + world + .entity_mut(seat_entity) + .insert(ecs::ActiveClients(vec![client_entity])); + } } None => { - world.entity_mut(seat_entity).remove::(); + world.entity_mut(seat_entity).remove::(); } } } -pub fn set_next_client( +pub fn set_next_clients( world: &mut world::World, seat_entity: entity::Entity, - client_entity: Option, + clients_entities: Option>, ) { - match client_entity { - Some(client_entity) => { + match clients_entities { + Some(clients_entities) => { world .entity_mut(seat_entity) - .insert(ecs::NextClient(client_entity)); + .insert(ecs::NextClients(clients_entities)); } None => { - world.entity_mut(seat_entity).remove::(); + world.entity_mut(seat_entity).remove::(); } } } @@ -269,10 +280,10 @@ pub fn remove_client( client_entity: entity::Entity, ) -> anyhow::Result<()> { if world - .get::(seat_entity) - .is_some_and(|next_client| next_client.0 == client_entity) + .get::(seat_entity) + .is_some_and(|next_clients| next_clients.0.contains(&client_entity)) { - set_next_client(world, seat_entity, None); + set_next_clients(world, seat_entity, None); } let session_id = world @@ -292,13 +303,21 @@ pub fn remove_client( } let was_current = client::is_active(world, seat_entity, client_entity); - if was_current { - set_active_client(world, seat_entity, None); - let _ = activate(world, seat_entity); + if was_current + && let Some(mut active_clients) = world.get_mut::(seat_entity) + { + active_clients.0.retain(|client| *client != client_entity); + let no_active_clients = active_clients.0.is_empty(); + if no_active_clients { + set_active_client(world, seat_entity, None); + let _ = activate(world, seat_entity); + } } if is_vt_bound(world, seat_entity) { - let has_active_client = world.get::(seat_entity).is_some(); + let has_active_client = world + .get::(seat_entity) + .is_some_and(|clients| !clients.0.is_empty()); if was_current && !has_active_client { log::debug!("Closing active VT"); let vt = session_id.map(session::SessionId::as_raw).or_else(|| { @@ -314,6 +333,11 @@ pub fn remove_client( } } else if let Some(session_id) = session_id && !was_current + && !clients(world, seat_entity).into_iter().any(|client| { + world + .get::(client) + .is_some_and(|session| session.0 == session_id) + }) { log::debug!("Closing inactive VT"); vt_close(session_id.as_raw())?; @@ -452,51 +476,59 @@ fn send_session_switched(world: &world::World, seat_entity: entity::Entity, sess } pub fn activate(world: &mut world::World, seat_entity: entity::Entity) -> anyhow::Result<()> { - if world.get::(seat_entity).is_some() { + if world + .get::(seat_entity) + .is_some_and(|clients| !clients.0.is_empty()) + { return Ok(()); } let name = name(world, seat_entity)?; - let next_client = if let Some(next_client) = world.get::(seat_entity).copied() - { - log::debug!("Activating next queued client on {name}"); - set_next_client(world, seat_entity, None); - Some(next_client.0) - } else { - let clients = clients(world, seat_entity); - if clients.is_empty() { - log::info!("No clients on {name} to activate"); - return Err(anyhow::anyhow!("No clients on {name} to activate")); - } + let next_clients = + if let Some(next_clients) = world.get::(seat_entity).cloned() { + log::debug!("Activating next queued client on {name}"); + set_next_clients(world, seat_entity, None); + Some(next_clients.0) + } else { + let clients = clients(world, seat_entity); + if clients.is_empty() { + log::info!("No clients on {name} to activate"); + return Err(anyhow::anyhow!("No clients on {name} to activate")); + } + + if is_vt_bound(world, seat_entity) { + let cur_vt = world.get::(seat_entity).map(|vt| vt.0); + let clients = clients + .into_iter() + .filter(|client| { + world.get::(*client).is_some_and(|session| { + cur_vt.is_some_and(|vt| session.0.as_raw() == vt.get()) + }) + }) + .collect::>(); + if clients.is_empty() { + let cur_vt = cur_vt.map_or(0, num::NonZeroU32::get); + log::info!("No clients belonging to VT {cur_vt} to activate"); + return Err(anyhow::anyhow!( + "No clients belonging to VT {cur_vt} to activate" + )); + } - if is_vt_bound(world, seat_entity) { - let cur_vt = world.get::(seat_entity).map(|vt| vt.0); - let client = clients.iter().copied().find(|client| { - world - .get::(*client) - .is_some_and(|session| cur_vt.is_some_and(|vt| session.0.as_raw() == vt.get())) - }); - if client.is_some() { log::debug!( "Activating client belonging to VT {}", cur_vt.map_or(0, num::NonZeroU32::get) ); + Some(clients) } else { - let cur_vt = cur_vt.map_or(0, num::NonZeroU32::get); - log::info!("No clients belonging to VT {cur_vt} to activate"); - return Err(anyhow::anyhow!( - "No clients belonging to VT {cur_vt} to activate" - )); + log::debug!("Activating all clients on {name}"); + Some(clients) } - client - } else { - log::debug!("Activating first client on {name}"); - clients.first().copied() - } - }; + }; - if let Some(client) = next_client { - client::open(world, seat_entity, client)?; + if let Some(clients) = next_clients { + for client in clients { + client::open(world, seat_entity, client)?; + } } Ok(()) @@ -509,17 +541,20 @@ fn release_vt( refresh_current_vt(world, seat_entity); let cur_vt = world.get::(seat_entity).map(|vt| vt.0); let released_vt = world - .get::(seat_entity) - .and_then(|client| world.get::(client.0)) + .get::(seat_entity) + .and_then(|clients| clients.0.first()) + .and_then(|client| world.get::(*client)) .and_then(|session| num::NonZeroU32::new(session.0.as_raw())) .or(cur_vt); log::debug!("Releasing VT"); - if let Some(active_client) = world - .get::(seat_entity) - .map(|client| client.0) + if let Some(active_clients) = world + .get::(seat_entity) + .map(|clients| clients.0.clone()) { - client::disable(world, seat_entity, active_client)?; + for client in active_clients { + client::disable(world, seat_entity, client)?; + } } ack_vt_release(world, seat_entity)?; @@ -551,7 +586,10 @@ fn activate_vt( .insert(ecs::CurrentVt(acquired_vt)); notify_current_vt(world, seat_entity); - if world.get::(seat_entity).is_none() { + if world + .get::(seat_entity) + .is_none_or(|clients| clients.0.is_empty()) + { let has_clients = !clients(world, seat_entity).is_empty(); if has_clients { activate(world, seat_entity)?; diff --git a/daemon/src/tests/seat.rs b/daemon/src/tests/seat.rs index 695737a..c87f886 100644 --- a/daemon/src/tests/seat.rs +++ b/daemon/src/tests/seat.rs @@ -17,6 +17,16 @@ fn make_client(world: &mut World) -> Entity { .id() } +fn make_client_with_pid(world: &mut World, pid: u32) -> Entity { + world + .spawn(( + ecs::Client, + ecs::ClientPid(pid), + ecs::SeatState(seat::SeatState::New), + )) + .id() +} + #[test] fn add_assigns_session_id_1() { let mut world = test_world(); @@ -66,8 +76,69 @@ fn add_sets_active_client() { let client = make_client(&mut world); client::add(&mut world, seat, client).unwrap(); assert_eq!( - world.get::(seat).map(|a| a.0), - Some(client) + world.get::(seat).map(|a| a.0.clone()), + Some(vec![client]) + ); +} + +#[test] +fn add_keeps_multiple_clients_active() { + let mut world = test_world(); + let seat = make_seat(&mut world); + let c1 = make_client_with_pid(&mut world, 100); + let c2 = make_client_with_pid(&mut world, 200); + client::add(&mut world, seat, c1).unwrap(); + client::add(&mut world, seat, c2).unwrap(); + assert_eq!( + world + .get::(seat) + .map(|clients| clients.0.clone()), + Some(vec![c1, c2]) + ); +} + +#[test] +fn add_rejects_duplicate_pid() { + let mut world = test_world(); + let seat = make_seat(&mut world); + let c1 = make_client_with_pid(&mut world, 100); + let c2 = make_client_with_pid(&mut world, 100); + client::add(&mut world, seat, c1).unwrap(); + assert!(client::add(&mut world, seat, c2).is_err()); +} + +#[test] +fn removing_one_active_client_preserves_the_others() { + let mut world = test_world(); + let seat = make_seat(&mut world); + let c1 = make_client_with_pid(&mut world, 100); + let c2 = make_client_with_pid(&mut world, 200); + client::add(&mut world, seat, c1).unwrap(); + client::add(&mut world, seat, c2).unwrap(); + seat::remove_client(&mut world, seat, c1).unwrap(); + assert_eq!( + world + .get::(seat) + .map(|clients| clients.0.clone()), + Some(vec![c2]) + ); +} + +#[test] +fn acknowledging_one_disabled_client_preserves_other_active_clients() { + let mut world = test_world(); + let seat = make_seat(&mut world); + let c1 = make_client_with_pid(&mut world, 100); + let c2 = make_client_with_pid(&mut world, 200); + client::add(&mut world, seat, c1).unwrap(); + client::add(&mut world, seat, c2).unwrap(); + client::disable(&mut world, seat, c1).unwrap(); + client::ack_disable(&mut world, seat, c1).unwrap(); + assert_eq!( + world + .get::(seat) + .map(|clients| clients.0.clone()), + Some(vec![c2]) ); } @@ -162,12 +233,15 @@ fn ack_disable_activates_next_queued_client() { .entity_mut(c2) .insert(ecs::SeatName("seat0".to_string())); world.entity_mut(c2).insert(ecs::ClientSeat(seat)); - world.entity_mut(seat).insert(ecs::NextClient(c2)); + world.entity_mut(seat).insert(ecs::NextClients(vec![c2])); client::disable(&mut world, seat, c1).unwrap(); client::ack_disable(&mut world, seat, c1).unwrap(); - assert_eq!(world.get::(seat).map(|a| a.0), Some(c2)); + assert_eq!( + world.get::(seat).map(|a| a.0.clone()), + Some(vec![c2]) + ); } #[test] @@ -178,7 +252,7 @@ fn set_next_session_switches_to_self_is_noop() { client::add(&mut world, seat, client).unwrap(); let session_id = world.get::(client).unwrap().0.as_raw(); seat::set_next_session(&mut world, seat, client, session_id).unwrap(); - assert!(world.get::(seat).is_none()); + assert!(world.get::(seat).is_none()); } #[test] @@ -200,7 +274,10 @@ fn set_next_session_queues_switch() { client::add(&mut world, seat, c2).unwrap(); let c1_session = world.get::(c1).unwrap().0.as_raw(); seat::set_next_session(&mut world, seat, c2, c1_session).unwrap(); - assert_eq!(world.get::(seat).map(|n| n.0), Some(c1)); + assert_eq!( + world.get::(seat).map(|n| n.0.clone()), + Some(vec![c1]) + ); } #[test] @@ -210,6 +287,6 @@ fn remove_client_cleans_up() { let client = make_client(&mut world); client::add(&mut world, seat, client).unwrap(); seat::remove_client(&mut world, seat, client).unwrap(); - assert!(world.get::(seat).is_none()); + assert!(world.get::(seat).is_none()); assert!(seat::clients(&world, seat).is_empty()); }