rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::time::Instant;

use rmux_proto::{
    decode_internal_pane_exit_probe, CommandOutput, ListPanesRequest, ListPanesResponse, PaneId,
    Response, SessionId,
};

use super::super::RequestHandler;
use crate::pane_terminals::PaneExitMetadata;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PaneExitProbeObservation {
    pane_id: PaneId,
    metadata: Option<PaneExitMetadata>,
    retained: bool,
}

impl RequestHandler {
    pub(in crate::handler) async fn handle_internal_pane_exit_probe(
        &self,
        request: &ListPanesRequest,
    ) -> Option<Response> {
        let (session_id, pane_id) = request
            .format
            .as_deref()
            .and_then(decode_internal_pane_exit_probe)?;

        let observation = self
            .live_pane_exit_probe(session_id, pane_id)
            .await
            .or_else(|| {
                self.retained_exited_pane_output_by_id(pane_id, Instant::now())
                    .map(|retained| PaneExitProbeObservation {
                        pane_id,
                        metadata: Some(retained.metadata()),
                        retained: true,
                    })
            });
        let stdout = observation.map_or_else(Vec::new, render_pane_exit_probe);
        Some(Response::ListPanes(ListPanesResponse {
            output: CommandOutput::from_stdout(stdout),
        }))
    }

    async fn live_pane_exit_probe(
        &self,
        session_id: SessionId,
        pane_id: PaneId,
    ) -> Option<PaneExitProbeObservation> {
        let state = self.state.lock().await;
        let (session_name, session) = state
            .sessions
            .iter()
            .find(|(_name, session)| session.id() == session_id)?;
        session.window_index_for_pane_id(pane_id)?;
        Some(PaneExitProbeObservation {
            pane_id,
            metadata: state.pane_exit_metadata(session_name, pane_id),
            retained: false,
        })
    }
}

fn render_pane_exit_probe(observation: PaneExitProbeObservation) -> Vec<u8> {
    let (dead, status, signal) = match observation.metadata {
        Some(metadata) => (
            1,
            metadata
                .status
                .map_or_else(String::new, |value| value.to_string()),
            metadata
                .signal
                .map_or_else(String::new, |value| value.to_string()),
        ),
        None => (0, String::new(), String::new()),
    };
    let retained = u8::from(observation.retained);
    format!(
        "{}\t{dead}\t{status}\t{signal}\t{retained}\n",
        observation.pane_id
    )
    .into_bytes()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn live_probe_has_the_existing_process_state_shape() {
        assert_eq!(
            render_pane_exit_probe(PaneExitProbeObservation {
                pane_id: PaneId::new(9),
                metadata: None,
                retained: false,
            }),
            b"%9\t0\t\t\t0\n"
        );
    }

    #[test]
    fn retained_probe_preserves_process_exit_details() {
        assert_eq!(
            render_pane_exit_probe(PaneExitProbeObservation {
                pane_id: PaneId::new(9),
                metadata: Some(PaneExitMetadata {
                    status: Some(7),
                    signal: None,
                    time: Some(1),
                }),
                retained: true,
            }),
            b"%9\t1\t7\t\t1\n"
        );
    }
}