aboutsummaryrefslogtreecommitdiff
path: root/src/client/endpoint/activation
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/endpoint/activation')
-rw-r--r--src/client/endpoint/activation/model.rs174
-rw-r--r--src/client/endpoint/activation/protocol.rs219
2 files changed, 393 insertions, 0 deletions
diff --git a/src/client/endpoint/activation/model.rs b/src/client/endpoint/activation/model.rs
new file mode 100644
index 00000000..c94029e9
--- /dev/null
+++ b/src/client/endpoint/activation/model.rs
@@ -0,0 +1,174 @@
+use std::time::Instant;
+
+use super::super::ClientEndpointId;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(super) struct EndpointLease {
+ pub(super) endpoint_id: ClientEndpointId,
+ pub(super) generation: u64,
+ pub(super) boot_id: String,
+ /// The endpoint cache may already be newer than the first activation event. Never let an
+ /// activation prove coherence with a revision that the monotonic cache has discarded.
+ pub(super) minimum_revision: u64,
+}
+
+#[derive(Clone, Debug, Default)]
+pub(super) struct ActivationEvidence {
+ pub(super) snapshot_revision: Option<u64>,
+ pub(super) focused_workspace_id: Option<String>,
+ pub(super) focused_tab_id: Option<String>,
+ pub(super) focused_pane_id: Option<String>,
+ pub(super) surface: Option<crate::protocol::PaneSurfaceFrame>,
+}
+
+impl ActivationEvidence {
+ pub(super) fn record_snapshot(&mut self, snapshot: &crate::protocol::ClientShellSnapshot) {
+ if self
+ .snapshot_revision
+ .is_none_or(|current| snapshot.revision >= current)
+ {
+ self.snapshot_revision = Some(snapshot.revision);
+ self.focused_workspace_id = snapshot.focused_workspace_id.clone();
+ self.focused_tab_id = snapshot.focused_tab_id.clone();
+ self.focused_pane_id = snapshot.focused_pane_id.clone();
+ }
+ }
+
+ pub(super) fn record_surface(&mut self, surface: crate::protocol::PaneSurfaceFrame) {
+ let replace = self.surface.as_ref().is_none_or(|current| {
+ surface.projection_revision > current.projection_revision
+ || (surface.projection_revision == current.projection_revision
+ && surface.surface_revision >= current.surface_revision)
+ });
+ if replace {
+ self.surface = Some(surface);
+ }
+ }
+
+ pub(super) fn invalidate_surface(&mut self) {
+ self.surface = None;
+ }
+
+ pub(super) fn coherent_surface(
+ &self,
+ minimum_revision: u64,
+ geometry: crate::protocol::ClientSurfaceSize,
+ ) -> Option<&crate::protocol::PaneSurfaceFrame> {
+ self.surface.as_ref().filter(|surface| {
+ self.snapshot_revision == Some(surface.projection_revision)
+ && surface.projection_revision >= minimum_revision
+ && super::surface_matches_geometry(surface, geometry)
+ })
+ }
+}
+
+#[derive(Clone, Debug)]
+pub(super) enum ActivationPhase {
+ ReleasingSource {
+ request_id: String,
+ },
+ ActivatingTarget {
+ request_id: String,
+ acknowledged_revision: Option<u64>,
+ /// At most one focus request may be in flight. Retargets only replace `focus` until
+ /// this response arrives, at which point the latest target is sent.
+ focus_request_id: Option<String>,
+ focus_request_target: Option<crate::client::shell::ClientEndpointFocusTarget>,
+ focus_acknowledged: bool,
+ evidence: ActivationEvidence,
+ },
+ ReleasingTargetForRollback {
+ request_id: String,
+ },
+ RestoringSource {
+ request_id: String,
+ acknowledged_revision: Option<u64>,
+ evidence: ActivationEvidence,
+ },
+ SynchronizingPresentation {
+ lease: EndpointLease,
+ request_id: String,
+ acknowledged_revision: Option<u64>,
+ evidence: ActivationEvidence,
+ completion: Box<ActivationCompletion>,
+ },
+ AwaitingPresentationEffects {
+ lease: EndpointLease,
+ token: String,
+ ready: bool,
+ completion: Box<ActivationCompletion>,
+ },
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum SurfaceActivationProgress {
+ Pending,
+ Ready,
+ Rejected {
+ message: String,
+ source_release_rejected: bool,
+ },
+ Stale,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum ActivationRollback {
+ /// A correlated rollback request is in flight; retain the frozen frame.
+ Pending,
+ /// Neither endpoint can be made safe to present. Keep pane input frozen while presenting
+ /// client chrome and this error.
+ Unavailable(String),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum ActivationCompletion {
+ AwaitingPresentationSync {
+ previous: ClientEndpointId,
+ endpoint: ClientEndpointId,
+ },
+ AwaitingPresentationEffects,
+ Activated,
+ RestoredSource {
+ error: String,
+ /// A newer endpoint-qualified selection arrived while this handoff was frozen. It is
+ /// started only after the original source has been coherently restored.
+ successor: Option<EndpointActivationIntent>,
+ },
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) struct EndpointActivationIntent {
+ pub(crate) endpoint_id: ClientEndpointId,
+ pub(crate) target: Option<crate::client::shell::ClientEndpointFocusTarget>,
+}
+
+/// Begin failures are separated by whether the transport may already have observed a lifecycle
+/// write. A partial transaction must be kept and rolled back under a frozen presentation.
+#[derive(Debug)]
+pub(crate) enum ActivationBeginError {
+ Preflight(String),
+ Partial {
+ activation: Box<PendingEndpointActivation>,
+ error: String,
+ },
+}
+
+/// The only owner of an endpoint handoff. The registry's active endpoint remains the committed
+/// endpoint until the target commits, while this object owns the uncommitted lifecycle lane.
+#[derive(Debug)]
+pub(crate) struct PendingEndpointActivation {
+ pub(super) source: EndpointLease,
+ pub(super) source_available: bool,
+ pub(super) target: EndpointLease,
+ pub(super) focus: Option<crate::client::shell::ClientEndpointFocusTarget>,
+ pub(super) host_focused: bool,
+ pub(super) resize: crate::protocol::ClientMessage,
+ pub(super) phase: ActivationPhase,
+ pub(super) deadline: Instant,
+ pub(super) epoch: u64,
+ pub(super) next_focus_serial: u64,
+ pub(super) rollback_error: Option<String>,
+ /// A different endpoint was selected while this source-off-first transaction was in flight.
+ /// Keep only the latest intent until source restoration commits.
+ pub(super) successor: Option<EndpointActivationIntent>,
+}
diff --git a/src/client/endpoint/activation/protocol.rs b/src/client/endpoint/activation/protocol.rs
new file mode 100644
index 00000000..569d53f3
--- /dev/null
+++ b/src/client/endpoint/activation/protocol.rs
@@ -0,0 +1,219 @@
+use super::super::{ClientEndpointId, EndpointRegistry, EndpointSendOutcome};
+use super::model::{ActivationEvidence, EndpointLease};
+
+pub(super) fn focus_result_matches(
+ focus: Option<&crate::client::shell::ClientEndpointFocusTarget>,
+ result: &crate::api::schema::ResponseResult,
+) -> bool {
+ match (focus, result) {
+ (
+ Some(crate::client::shell::ClientEndpointFocusTarget::Pane(expected)),
+ crate::api::schema::ResponseResult::PaneInfo { pane },
+ ) => pane.focused && &pane.pane_id == expected,
+ (
+ Some(crate::client::shell::ClientEndpointFocusTarget::Workspace(expected)),
+ crate::api::schema::ResponseResult::WorkspaceInfo { workspace },
+ ) => workspace.focused && &workspace.workspace_id == expected,
+ (
+ Some(crate::client::shell::ClientEndpointFocusTarget::Tab(expected)),
+ crate::api::schema::ResponseResult::TabInfo { tab },
+ ) => tab.focused && &tab.tab_id == expected,
+ _ => false,
+ }
+}
+
+pub(super) fn endpoint_lease(
+ shell: &crate::client::shell::ClientShellState,
+ endpoints: &EndpointRegistry,
+ endpoint_id: &ClientEndpointId,
+) -> Result<EndpointLease, String> {
+ let connection = endpoints
+ .connection(endpoint_id)
+ .ok_or_else(|| "endpoint connection is unavailable".to_owned())?;
+ let (boot_id, minimum_revision) = shell
+ .endpoint_snapshot_identity(endpoint_id, connection.generation)
+ .ok_or_else(|| "endpoint metadata is not ready for this connection".to_owned())?;
+ Ok(EndpointLease {
+ endpoint_id: endpoint_id.clone(),
+ generation: connection.generation,
+ boot_id: boot_id.to_owned(),
+ minimum_revision,
+ })
+}
+
+pub(super) fn disconnected_endpoint_lease(
+ shell: &crate::client::shell::ClientShellState,
+ endpoint_id: &ClientEndpointId,
+) -> EndpointLease {
+ EndpointLease {
+ endpoint_id: endpoint_id.clone(),
+ generation: 0,
+ boot_id: shell
+ .endpoint_boot_id(endpoint_id)
+ .unwrap_or_default()
+ .to_owned(),
+ minimum_revision: 0,
+ }
+}
+
+pub(super) fn endpoint_matches(
+ lease: &EndpointLease,
+ endpoint_id: &ClientEndpointId,
+ generation: u64,
+ boot_id: &str,
+) -> bool {
+ lease.endpoint_id == *endpoint_id && lease.generation == generation && lease.boot_id == boot_id
+}
+
+pub(super) fn coherent_completion_surface(
+ shell: &crate::client::shell::ClientShellState,
+ lease: &EndpointLease,
+ evidence: &ActivationEvidence,
+ acknowledgement_revision: Option<u64>,
+ geometry: crate::protocol::ClientSurfaceSize,
+) -> Result<crate::protocol::PaneSurfaceFrame, String> {
+ let acknowledgement_revision = acknowledgement_revision.ok_or_else(|| {
+ "endpoint activation completed without a surface acknowledgement".to_owned()
+ })?;
+ let surface = evidence
+ .surface
+ .clone()
+ .ok_or_else(|| "endpoint activation completed without a surface".to_owned())?;
+ if surface.projection_revision < acknowledgement_revision
+ || !surface_matches_geometry(&surface, geometry)
+ {
+ return Err("endpoint activation lost its acknowledged surface evidence".into());
+ }
+ if !shell.endpoint_snapshot_matches(
+ &lease.endpoint_id,
+ lease.generation,
+ &lease.boot_id,
+ surface.projection_revision,
+ ) {
+ return Err("endpoint activation lost its coherent snapshot/surface pair".into());
+ }
+ Ok(surface)
+}
+
+pub(super) fn resize_geometry(
+ message: &crate::protocol::ClientMessage,
+) -> Option<crate::protocol::ClientSurfaceSize> {
+ match message {
+ crate::protocol::ClientMessage::ClientShellResize { surface_size, .. } => {
+ Some(*surface_size)
+ }
+ _ => None,
+ }
+}
+
+pub(super) fn surface_matches_geometry(
+ surface: &crate::protocol::PaneSurfaceFrame,
+ geometry: crate::protocol::ClientSurfaceSize,
+) -> bool {
+ surface.frame.width == geometry.cols && surface.frame.height == geometry.rows
+}
+
+pub(super) fn send_surface_activation(
+ endpoints: &mut EndpointRegistry,
+ target: &EndpointLease,
+ request_id: String,
+ resize: &crate::protocol::ClientMessage,
+ focused: bool,
+) -> Result<(), String> {
+ if endpoints.send_to(&target.endpoint_id, resize) != EndpointSendOutcome::Sent {
+ return Err("endpoint resize could not be sent".into());
+ }
+ let request = surface_interest_request(&target.boot_id, request_id, true)
+ .map_err(|error| error.to_string())?;
+ if endpoints.send_to(&target.endpoint_id, &request) != EndpointSendOutcome::Sent {
+ return Err("endpoint activation could not be sent".into());
+ }
+ // Inactive endpoints reject focus events. Activate first, then establish the host baseline
+ // on the same ordered transport before navigation or presentation can commit.
+ if endpoints.send_to(
+ &target.endpoint_id,
+ &crate::protocol::ClientMessage::ClientShellFocus { focused },
+ ) != EndpointSendOutcome::Sent
+ {
+ return Err("endpoint focus baseline could not be sent".into());
+ }
+ Ok(())
+}
+
+pub(super) fn decode_endpoint_response(
+ request_id: &str,
+ data: &[u8],
+) -> Result<crate::api::schema::ResponseResult, crate::client::shell::ClientShellEndpointError> {
+ crate::client::endpoint_commands::parse_response(request_id, data)
+}
+
+pub(super) fn surface_set_revision(
+ result: &crate::api::schema::ResponseResult,
+ expected_active: bool,
+) -> Result<u64, String> {
+ match result {
+ crate::api::schema::ResponseResult::ClientShellSurfaceSet {
+ active,
+ projection_revision,
+ } if *active == expected_active => Ok(*projection_revision),
+ _ => Err("surface activation returned an invalid acknowledgement".into()),
+ }
+}
+
+pub(super) fn focus_request(
+ boot_id: &str,
+ request_id: String,
+ focus: &crate::client::shell::ClientEndpointFocusTarget,
+) -> std::io::Result<crate::protocol::ClientMessage> {
+ let method = match focus {
+ crate::client::shell::ClientEndpointFocusTarget::Workspace(workspace_id) => {
+ crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget {
+ workspace_id: workspace_id.clone(),
+ })
+ }
+ crate::client::shell::ClientEndpointFocusTarget::Pane(pane_id) => {
+ crate::api::schema::Method::PaneFocus(crate::api::schema::PaneTarget {
+ pane_id: pane_id.clone(),
+ })
+ }
+ crate::client::shell::ClientEndpointFocusTarget::Tab(tab_id) => {
+ crate::api::schema::Method::TabFocus(crate::api::schema::TabTarget {
+ tab_id: tab_id.clone(),
+ })
+ }
+ };
+ endpoint_request(
+ boot_id,
+ crate::api::schema::Request {
+ id: request_id,
+ method,
+ },
+ )
+}
+
+pub(super) fn surface_interest_request(
+ boot_id: &str,
+ request_id: String,
+ active: bool,
+) -> std::io::Result<crate::protocol::ClientMessage> {
+ endpoint_request(
+ boot_id,
+ crate::api::schema::Request {
+ id: request_id,
+ method: crate::api::schema::Method::ClientShellSurfaceSet(
+ crate::api::schema::ClientShellSurfaceSetParams { active },
+ ),
+ },
+ )
+}
+
+fn endpoint_request(
+ boot_id: &str,
+ request: crate::api::schema::Request,
+) -> std::io::Result<crate::protocol::ClientMessage> {
+ Ok(crate::protocol::ClientMessage::ClientShellEndpointRequest {
+ boot_id: boot_id.to_owned(),
+ request: serde_json::to_string(&request)
+ .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?,
+ })
+}