rmux-server 0.9.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::LifecycleEvent;
use rmux_proto::request::RefreshClientRequest;
use rmux_proto::{
    ErrorResponse, RefreshClientResponse, Response, RmuxError, SessionId, TerminalSize,
    WindowTarget,
};

use crate::handler_support::attached_client_required;
use crate::pane_io::AttachControl;

use super::super::{
    client_runtime_support::clipboard_query_sequence,
    control_support::{ControlClientIdentity, ManagedClient},
    RequestHandler,
};

impl RequestHandler {
    pub(in crate::handler) async fn handle_refresh_client(
        &self,
        requester_pid: u32,
        request: RefreshClientRequest,
    ) -> Response {
        if let Err(error) = validate_refresh_supported_request(&request) {
            return Response::Error(ErrorResponse { error });
        }

        let client = match self
            .resolve_target_managed_client(
                requester_pid,
                request.target_client.as_deref(),
                "refresh-client",
            )
            .await
        {
            Ok(client) => client,
            Err(error) => return Response::Error(ErrorResponse { error }),
        };

        match client {
            ManagedClient::Attach {
                pid: attach_pid,
                attach_id,
            } => {
                self.handle_refresh_attached_client(attach_pid, attach_id, request)
                    .await
            }
            ManagedClient::Control(identity) => {
                self.handle_refresh_control_client(identity, request).await
            }
        }
    }

    async fn handle_refresh_attached_client(
        &self,
        attach_pid: u32,
        expected_attach_id: u64,
        request: RefreshClientRequest,
    ) -> Response {
        let mut needs_full_refresh = !request.status_only;
        let clipboard_query = request.clipboard_query;
        let (session_name, session_id, size_eligibility_changed) = {
            let mut active_attach = self.active_attach.lock().await;
            let Some(active) = active_attach.by_pid.get_mut(&attach_pid).filter(|active| {
                active.id == expected_attach_id
                    && !active.closing.load(std::sync::atomic::Ordering::SeqCst)
            }) else {
                return Response::Error(ErrorResponse {
                    error: attached_client_required("refresh-client"),
                });
            };

            let ignored_size_before = active.flags.contains(super::ClientFlags::IGNORESIZE);
            let raw_flag = request.flags.as_deref().or(request.flags_alias.as_deref());
            if let Some(raw) = raw_flag {
                let mut merged_flags = active.flags;
                for token in raw.split(',').filter(|t| !t.is_empty()) {
                    if let Err(error) = merged_flags.apply_named(token) {
                        return Response::Error(ErrorResponse { error });
                    }
                }
                if !active.can_write {
                    merged_flags = merged_flags.with_read_only();
                }
                active.flags = merged_flags;
            }
            let size_eligibility_changed =
                ignored_size_before != active.flags.contains(super::ClientFlags::IGNORESIZE);
            if size_eligibility_changed {
                self.bump_active_attach_epoch();
            }

            (
                active.session_name.clone(),
                active.session_id,
                size_eligibility_changed,
            )
        };

        if size_eligibility_changed {
            if let Err(error) = self
                .reconcile_refresh_size_eligibility_transition(
                    attach_pid,
                    expected_attach_id,
                    session_id,
                )
                .await
            {
                return Response::Error(ErrorResponse { error });
            }
        }

        if request.status_only {
            if let Err(error) = self
                .refresh_attached_client_status_for_identity(
                    attach_pid,
                    expected_attach_id,
                    &session_name,
                )
                .await
            {
                return Response::Error(ErrorResponse { error });
            }
            needs_full_refresh = false;
        }
        if clipboard_query {
            if let Err(error) = self
                .send_attach_control_for_client_identity(
                    attach_pid,
                    expected_attach_id,
                    AttachControl::Write(clipboard_query_sequence()),
                    "refresh-client",
                )
                .await
            {
                return Response::Error(ErrorResponse { error });
            }
        }
        if needs_full_refresh {
            if let Err(error) = self
                .refresh_attached_client_for_identity(
                    attach_pid,
                    expected_attach_id,
                    &session_name,
                    "refresh-client",
                )
                .await
            {
                return Response::Error(ErrorResponse { error });
            }
        }

        Response::RefreshClient(RefreshClientResponse {
            target_client: attach_pid.to_string(),
        })
    }

    async fn reconcile_refresh_size_eligibility_transition(
        &self,
        attach_pid: u32,
        expected_attach_id: u64,
        expected_session_id: SessionId,
    ) -> Result<(), RmuxError> {
        let identity_is_current = {
            let active_attach = self.active_attach.lock().await;
            active_attach.by_pid.get(&attach_pid).is_some_and(|active| {
                active.id == expected_attach_id
                    && active.session_id == expected_session_id
                    && !active.closing.load(std::sync::atomic::Ordering::SeqCst)
            })
        };
        if !identity_is_current {
            return Err(attached_client_required("refresh-client"));
        }
        self.reconcile_attached_session_identity_size_and_emit(expected_session_id)
            .await
    }

    async fn handle_refresh_control_client(
        &self,
        identity: ControlClientIdentity,
        request: RefreshClientRequest,
    ) -> Response {
        let control_pid = identity.requester_pid();
        if request.has_attach_only_effects() {
            return Response::Error(ErrorResponse {
                error: attached_client_required("refresh-client"),
            });
        }

        let control_size = match request.control_size.as_deref() {
            Some(value) => match parse_control_size(value) {
                Some(size) => Some(size),
                None => {
                    return Response::Error(ErrorResponse {
                        error: RmuxError::Server(format!("invalid refresh-client size '{value}'")),
                    });
                }
            },
            None => None,
        };

        let (session_name, session_id) = {
            let active_control = self.active_control.lock().await;
            let Some(active) = active_control.by_pid.get(&control_pid).filter(|active| {
                active.id == identity.control_id()
                    && !active.closing.load(std::sync::atomic::Ordering::SeqCst)
            }) else {
                return Response::Error(ErrorResponse {
                    error: attached_client_required("refresh-client"),
                });
            };
            (active.session_name.clone(), active.session_id)
        };

        if let (Some(session_name), Some(session_id), Some(size)) =
            (session_name.as_ref(), session_id, control_size)
        {
            #[cfg(windows)]
            self.wait_for_windows_deferred_all_pane_pids().await;
            let target = {
                let mut state = self.state.lock().await;
                let active_control = self.active_control.lock().await;
                let exact_client_still_attached = active_control
                    .by_pid
                    .get(&control_pid)
                    .is_some_and(|active| {
                        active.id == identity.control_id()
                            && !active.closing.load(std::sync::atomic::Ordering::SeqCst)
                            && active.session_name.as_ref() == Some(session_name)
                            && active.session_id == Some(session_id)
                    });
                if !exact_client_still_attached {
                    return Response::Error(ErrorResponse {
                        error: attached_client_required("refresh-client"),
                    });
                }
                match state.mutate_session_and_resize_active_window_terminal(
                    session_name,
                    |session| {
                        session.touch_attached();
                        session.resize_active_window_terminal(size);
                        Ok(WindowTarget::with_window(
                            session_name.clone(),
                            session.active_window_index(),
                        ))
                    },
                ) {
                    Ok(target) => target,
                    Err(error) => return Response::Error(ErrorResponse { error }),
                }
            };
            self.emit(LifecycleEvent::WindowLayoutChanged { target })
                .await;
        } else if control_size.is_none() {
            if let Err(error) = self.refresh_control_client_for_identity(identity).await {
                return Response::Error(ErrorResponse { error });
            }
        }

        Response::RefreshClient(RefreshClientResponse {
            target_client: control_pid.to_string(),
        })
    }
}

trait RefreshClientControlScope {
    fn has_attach_only_effects(&self) -> bool;
}

impl RefreshClientControlScope for RefreshClientRequest {
    fn has_attach_only_effects(&self) -> bool {
        self.status_only
            || self.clipboard_query
            || self.flags.is_some()
            || self.flags_alias.is_some()
    }
}

fn validate_refresh_supported_request(request: &RefreshClientRequest) -> Result<(), RmuxError> {
    let mut unsupported = Vec::new();
    if request.clear_pan {
        unsupported.push("-c");
    }
    if request.pan_down {
        unsupported.push("-D");
    }
    if request.pan_left {
        unsupported.push("-L");
    }
    if request.pan_right {
        unsupported.push("-R");
    }
    if request.pan_up {
        unsupported.push("-U");
    }
    if request.adjustment.is_some() {
        unsupported.push("adjustment");
    }
    if !request.subscriptions.is_empty() {
        unsupported.push("-A");
    }
    if !request.subscriptions_format.is_empty() {
        unsupported.push("-B");
    }
    if request.colour_report.is_some() {
        unsupported.push("-r");
    }
    if unsupported.is_empty() {
        return Ok(());
    }
    Err(RmuxError::Server(format!(
        "refresh-client {} is not supported",
        unsupported.join("/")
    )))
}

fn parse_control_size(value: &str) -> Option<TerminalSize> {
    let (cols, rows) = value.split_once('x')?;
    let cols = cols.parse::<u16>().ok()?;
    let rows = rows.parse::<u16>().ok()?;
    (cols > 0 && rows > 0).then_some(TerminalSize { cols, rows })
}