rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::super::client_support::install_managed_client_resolution_pause;
use super::super::overlay_support::{AttachedOverlayInput, ClientOverlayState};
use super::super::scripting_support::QueueExecutionContext;
use super::super::RequestHandler;
use rmux_proto::{
    KillSessionRequest, LinkWindowRequest, NewSessionRequest, NewWindowRequest, PaneTarget,
    Request, RespawnPaneRequest, Response, SessionName, Target, TerminalSize, UnlinkWindowRequest,
    WindowTarget,
};
use tokio::sync::mpsc;
use tokio::time::{timeout, Duration};

fn session_name(value: &str) -> SessionName {
    SessionName::new(value).expect("valid session name")
}

async fn create_session(handler: &RequestHandler, name: &SessionName) {
    let response = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: name.clone(),
            detached: true,
            size: Some(TerminalSize { cols: 80, rows: 24 }),
            environment: None,
        }))
        .await;
    assert!(matches!(response, Response::NewSession(_)), "{response:?}");
}

async fn attach(
    handler: &RequestHandler,
    pid: u32,
    session: &SessionName,
) -> mpsc::UnboundedReceiver<crate::pane_io::AttachControl> {
    let (control_tx, control_rx) = mpsc::unbounded_channel();
    handler
        .register_attach(pid, session.clone(), control_tx)
        .await;
    control_rx
}

async fn execute_overlay(
    handler: &RequestHandler,
    requester_pid: u32,
    command: &str,
) -> Result<(), rmux_proto::RmuxError> {
    let parsed = handler.parse_control_commands(command).await?;
    handler
        .execute_parsed_commands_for_test(requester_pid, parsed)
        .await
        .map(|_| ())
}

async fn link_window(
    handler: &RequestHandler,
    owner: &SessionName,
    alias: &SessionName,
) -> Response {
    handler
        .handle(Request::LinkWindow(LinkWindowRequest {
            source: WindowTarget::with_window(owner.clone(), 0),
            target: WindowTarget::with_window(alias.clone(), 0),
            after: false,
            before: false,
            kill_destination: true,
            detached: true,
        }))
        .await
}

#[tokio::test]
async fn client_selector_and_explicit_pane_target_keep_distinct_identities() {
    let handler = RequestHandler::new();
    let alpha = session_name("overlay-selector-alpha");
    let beta = session_name("overlay-selector-beta");
    let alpha_pid = 971_001;
    let beta_pid = 971_002;
    create_session(&handler, &alpha).await;
    create_session(&handler, &beta).await;
    let _alpha_rx = attach(&handler, alpha_pid, &alpha).await;
    let _beta_rx = attach(&handler, beta_pid, &beta).await;

    execute_overlay(
        &handler,
        alpha_pid,
        &format!("display-menu -c {beta_pid} -t {alpha}:0.0 Item i 'display-message chosen'"),
    )
    .await
    .expect("menu opens on selected client");
    {
        let active_attach = handler.active_attach.lock().await;
        let Some(ClientOverlayState::Menu(menu)) = active_attach.by_pid[&beta_pid].overlay.as_ref()
        else {
            panic!("selected client owns the menu");
        };
        assert_eq!(
            menu.current_target,
            rmux_proto::Target::Pane(PaneTarget::with_window(alpha.clone(), 0, 0))
        );
    }
    handler
        .clear_interactive_overlay(beta_pid, true)
        .await
        .expect("menu cleanup");

    execute_overlay(
        &handler,
        alpha_pid,
        &format!("display-popup -N -c {beta_pid} -t {alpha}:0.0 -T Popup"),
    )
    .await
    .expect("popup opens on selected client");
    let active_attach = handler.active_attach.lock().await;
    let Some(ClientOverlayState::Popup(popup)) = active_attach.by_pid[&beta_pid].overlay.as_ref()
    else {
        panic!("selected client owns the popup");
    };
    assert_eq!(
        popup.current_target,
        rmux_proto::Target::Pane(PaneTarget::with_window(alpha, 0, 0))
    );
}

#[tokio::test]
async fn same_pid_replacement_during_client_resolution_cannot_receive_menu() {
    let handler = RequestHandler::new();
    let alpha = session_name("overlay-client-aba");
    let pid = 971_011;
    create_session(&handler, &alpha).await;
    let _old_rx = attach(&handler, pid, &alpha).await;
    let pause = install_managed_client_resolution_pause(pid);
    let parsed = handler
        .parse_control_commands(&format!(
            "display-menu -c {pid} Item i 'display-message chosen'"
        ))
        .await
        .expect("menu parses");
    let task_handler = handler.clone();
    let task = tokio::spawn(async move {
        task_handler
            .execute_parsed_commands_for_test(pid, parsed)
            .await
    });
    timeout(Duration::from_secs(2), pause.reached.notified())
        .await
        .expect("client resolution pauses");

    let _replacement_rx = attach(&handler, pid, &alpha).await;
    pause.release.notify_one();
    let result = timeout(Duration::from_secs(2), task)
        .await
        .expect("menu command finishes")
        .expect("menu task does not panic");
    assert!(result.is_err(), "stale client identity must fail closed");
    assert!(handler.active_attach.lock().await.by_pid[&pid]
        .overlay
        .is_none());
}

#[tokio::test]
async fn menu_action_is_discarded_after_target_pane_respawn() {
    let handler = RequestHandler::new();
    let alpha = session_name("overlay-pane-respawn");
    let pid = 971_021;
    create_session(&handler, &alpha).await;
    let _control_rx = attach(&handler, pid, &alpha).await;
    execute_overlay(
        &handler,
        pid,
        &format!("display-menu -t {alpha}:0.0 Item i 'display-message should-not-run'"),
    )
    .await
    .expect("menu opens");

    let response = handler
        .handle(Request::RespawnPane(Box::new(RespawnPaneRequest {
            target: PaneTarget::with_window(alpha.clone(), 0, 0),
            kill: true,
            start_directory: None,
            environment: None,
            command: Some(vec![crate::test_shell::stdin_discard_command()]),
            process_command: None,
        })))
        .await;
    assert!(matches!(response, Response::RespawnPane(_)), "{response:?}");
    handler
        .handle_attached_live_input_for_test(pid, b"i")
        .await
        .expect("stale menu input is consumed");
    assert!(handler.active_attach.lock().await.by_pid[&pid]
        .overlay
        .is_none());
}

#[tokio::test]
async fn popup_is_discarded_after_same_window_is_unlinked_and_relinked() {
    let handler = RequestHandler::new();
    let host = session_name("overlay-relink-host");
    let owner = session_name("overlay-relink-owner");
    let alias = session_name("overlay-relink-alias");
    let pid = 971_031;
    for session in [&host, &owner, &alias] {
        create_session(&handler, session).await;
    }
    let _control_rx = attach(&handler, pid, &host).await;
    let extra = handler
        .handle(Request::NewWindow(Box::new(NewWindowRequest {
            target: alias.clone(),
            name: None,
            detached: true,
            start_directory: None,
            environment: None,
            command: None,
            process_command: None,
            target_window_index: Some(1),
            insert_at_target: false,
        })))
        .await;
    assert!(matches!(extra, Response::NewWindow(_)), "{extra:?}");
    let relinked = link_window(&handler, &owner, &alias).await;
    assert!(matches!(relinked, Response::LinkWindow(_)), "{relinked:?}");
    execute_overlay(
        &handler,
        pid,
        &format!("display-popup -N -t {alias}:0.0 -T Popup"),
    )
    .await
    .expect("popup opens on linked pane");

    let unlinked = handler
        .handle(Request::UnlinkWindow(UnlinkWindowRequest {
            target: WindowTarget::with_window(alias.clone(), 0),
            kill_if_last: false,
        }))
        .await;
    assert!(
        matches!(unlinked, Response::UnlinkWindow(_)),
        "{unlinked:?}"
    );
    let relinked = handler
        .handle(Request::LinkWindow(LinkWindowRequest {
            source: WindowTarget::with_window(owner.clone(), 0),
            target: WindowTarget::with_window(alias.clone(), 1),
            after: false,
            before: true,
            kill_destination: false,
            detached: true,
        }))
        .await;
    assert!(matches!(relinked, Response::LinkWindow(_)), "{relinked:?}");

    let mut pending = Vec::new();
    let outcome = handler
        .handle_attached_overlay_input(pid, &mut pending, b"x")
        .await
        .expect("stale popup input is consumed");
    assert_eq!(outcome, AttachedOverlayInput::Consumed);
    assert!(pending.is_empty());
    assert!(handler.active_attach.lock().await.by_pid[&pid]
        .overlay
        .is_none());
}

#[tokio::test]
async fn overlay_command_context_rejects_a_same_name_session_replacement_after_validation() {
    let handler = RequestHandler::new();
    let host = session_name("overlay-command-host");
    let target = session_name("overlay-command-target");
    let pid = 971_041;
    create_session(&handler, &host).await;
    create_session(&handler, &target).await;
    let _control_rx = attach(&handler, pid, &host).await;
    let client = handler.active_attach_identity_for_test(pid).await;
    let target = Target::Session(target);
    let identity = {
        let mut state = handler.state.lock().await;
        let active_attach = handler.active_attach.lock().await;
        let active = active_attach
            .by_pid
            .get(&pid)
            .expect("client remains attached");
        let identity =
            super::identity::OverlayIdentity::capture(&mut state, client, target.clone())
                .expect("overlay target identity captures");
        assert!(identity.matches(&state, active, &target));
        identity
    };
    let context =
        identity.command_context(QueueExecutionContext::without_caller_cwd(), target.clone());

    let target_name = target.session_name().clone();
    let killed = handler
        .handle(Request::KillSession(KillSessionRequest {
            target: target_name.clone(),
            kill_all_except_target: false,
            clear_alerts: false,
            kill_group: false,
        }))
        .await;
    assert!(matches!(killed, Response::KillSession(_)), "{killed:?}");
    create_session(&handler, &target_name).await;

    let commands = handler
        .parse_control_commands("set-buffer -b overlay-command-aba fired")
        .await
        .expect("overlay command parses");
    let result = handler
        .execute_parsed_commands(pid, commands, context)
        .await;
    assert!(result.is_err(), "replacement session must fail closed");
    assert!(handler
        .state
        .lock()
        .await
        .buffers
        .show(Some("overlay-command-aba"))
        .is_err());
}