rmux 0.7.1

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use std::path::Path;
use std::process::Command;
use std::time::Duration;

use rmux_proto::{KillSessionRequest, Response, SessionName};

use crate::cli_args::WithSessionArgs;
use crate::cli_response::tmux_cli_error_message;

use super::super::ExitFailure;
use super::common::{connect_cli, duration_millis, sleep_poll_interval};

pub(crate) fn run_with_session(
    args: WithSessionArgs,
    socket_path: &Path,
) -> Result<i32, ExitFailure> {
    if args.command.is_empty() {
        return Err(ExitFailure::new(
            1,
            "with-session requires a child command".to_owned(),
        ));
    }

    let ttl_millis = duration_millis(args.ttl);
    let mut connection = connect_cli(socket_path)?;
    let lease = create_lease(&mut connection, args.session_name.clone(), ttl_millis)?;
    let mut child = match Command::new(&args.command[0])
        .args(&args.command[1..])
        .env("RMUX_SESSION", args.session_name.as_str())
        .spawn()
    {
        Ok(child) => child,
        Err(error) => {
            let _ = release_lease(&mut connection, args.session_name.clone(), lease.token);
            return Err(ExitFailure::new(
                1,
                format!("with-session failed to spawn child: {error}"),
            ));
        }
    };

    let renew_interval = renew_interval(args.ttl);
    let mut time_to_renew = renew_interval;
    loop {
        match child.try_wait() {
            Ok(Some(status)) => {
                if args.kill_on_owner_exit {
                    kill_owned_session(&mut connection, args.session_name.clone())?;
                } else {
                    release_lease(&mut connection, args.session_name, lease.token)?;
                }
                return Ok(status.code().unwrap_or(1));
            }
            Ok(None) => {}
            Err(error) => {
                let _ = child.kill();
                let _ = release_lease(&mut connection, args.session_name, lease.token);
                return Err(ExitFailure::new(
                    1,
                    format!("with-session failed while waiting for child: {error}"),
                ));
            }
        }

        if time_to_renew <= super::common::POLL_INTERVAL {
            if let Err(error) = renew_lease(
                &mut connection,
                args.session_name.clone(),
                lease.token,
                ttl_millis,
            ) {
                let _ = child.kill();
                let _ = release_lease(&mut connection, args.session_name, lease.token);
                return Err(error);
            }
            time_to_renew = renew_interval;
        } else {
            time_to_renew = time_to_renew.saturating_sub(super::common::POLL_INTERVAL);
        }
        sleep_poll_interval();
    }
}

struct Lease {
    token: u64,
}

fn create_lease(
    connection: &mut rmux_client::Connection,
    session_name: SessionName,
    ttl_millis: u64,
) -> Result<Lease, ExitFailure> {
    match connection
        .create_session_lease(session_name, ttl_millis)
        .map_err(ExitFailure::from_client)?
    {
        Response::CreateSessionLease(response) => Ok(Lease {
            token: response.token,
        }),
        Response::Error(error) => Err(ExitFailure::new(
            1,
            tmux_cli_error_message("with-session", &error.error),
        )),
        other => Err(ExitFailure::new(
            1,
            format!(
                "protocol error: unexpected '{}' response for with-session",
                other.command_name()
            ),
        )),
    }
}

fn renew_lease(
    connection: &mut rmux_client::Connection,
    session_name: SessionName,
    token: u64,
    ttl_millis: u64,
) -> Result<(), ExitFailure> {
    match connection
        .renew_session_lease(session_name, token, ttl_millis)
        .map_err(ExitFailure::from_client)?
    {
        Response::RenewSessionLease(response) if response.renewed => Ok(()),
        Response::RenewSessionLease(_) => Err(ExitFailure::new(1, "with-session lease was lost")),
        Response::Error(error) => Err(ExitFailure::new(
            1,
            tmux_cli_error_message("with-session", &error.error),
        )),
        other => Err(ExitFailure::new(
            1,
            format!(
                "protocol error: unexpected '{}' response for with-session",
                other.command_name()
            ),
        )),
    }
}

fn release_lease(
    connection: &mut rmux_client::Connection,
    session_name: SessionName,
    token: u64,
) -> Result<(), ExitFailure> {
    match connection
        .release_session_lease(session_name, token)
        .map_err(ExitFailure::from_client)?
    {
        Response::ReleaseSessionLease(response) if response.released => Ok(()),
        Response::ReleaseSessionLease(_) => Err(ExitFailure::new(
            1,
            "with-session lease was already released or lost",
        )),
        Response::Error(error) => Err(ExitFailure::new(
            1,
            tmux_cli_error_message("with-session", &error.error),
        )),
        other => Err(ExitFailure::new(
            1,
            format!(
                "protocol error: unexpected '{}' response for with-session release",
                other.command_name()
            ),
        )),
    }
}

fn kill_owned_session(
    connection: &mut rmux_client::Connection,
    session_name: SessionName,
) -> Result<(), ExitFailure> {
    match connection
        .kill_session(KillSessionRequest {
            target: session_name,
            kill_all_except_target: false,
            clear_alerts: false,
        })
        .map_err(ExitFailure::from_client)?
    {
        Response::KillSession(_) => Ok(()),
        Response::Error(error)
            if matches!(error.error, rmux_proto::RmuxError::SessionNotFound(_)) =>
        {
            Ok(())
        }
        Response::Error(error) => Err(ExitFailure::new(
            1,
            tmux_cli_error_message("with-session", &error.error),
        )),
        other => Err(ExitFailure::new(
            1,
            format!(
                "protocol error: unexpected '{}' response for with-session cleanup",
                other.command_name()
            ),
        )),
    }
}

fn renew_interval(ttl: Duration) -> Duration {
    let third = ttl / 3;
    third.clamp(Duration::from_millis(100), Duration::from_secs(1))
}